about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--main.go348
1 files changed, 348 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..b8beb0e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,348 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"time"
+
+	"git.darknebu.la/emile/matrix"
+	"github.com/spf13/viper"
+	"github.com/wcharczuk/go-chart"
+	"gopkg.in/h2non/gentleman.v2"
+	"gopkg.in/h2non/gentleman.v2/plugins/query"
+)
+
+func main() {
+
+	// bot username / password / homeserver
+	initConfig()
+	username := viper.GetString("username")
+	password := viper.GetString("password")
+	homeserver := viper.GetString("homeserver")
+
+	// matrix login
+	fmt.Println("Logging in...")
+	authinfo, err := matrix.Login(username, password, homeserver)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// inital sync
+	fmt.Println("Initial sync...")
+	var syncResponse matrix.RespSync
+	syncResponse, err = matrix.Sync(authinfo)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// defining an events channel in which new events are inserted into, then
+	// processed. The events are packaged in a PackagedEvent containing the
+	// Event and the Roomname from where they where created. This makes it
+	// possible to send back the response to the room the request came from.
+	fmt.Println("Defining the events channel...")
+	events := make(chan matrix.PackagedEvent, 1000)
+
+	// incremental sync in an own thread (timeout of 1 second in the syncPartial
+	// function) pushing new events into the events channel for further
+	// processing
+	fmt.Println("Initializing periodic sync...")
+	go func() {
+		for {
+			syncResponse, err = matrix.SyncPartial(authinfo, syncResponse.NextBatch, events)
+			if err != nil {
+				fmt.Println(err)
+				return
+			}
+		}
+	}()
+
+	// periodically fetch new messages from the events channel as they arrive
+	// (this allows for optimal processing of the events, as the new events get
+	// DIRECTLY handled (as soon as all previous events are done being handled))
+	fmt.Println("Processing messages...")
+	for {
+		select {
+		case event := <-events:
+
+			// handle the message
+			switch event.Event.Content["body"] {
+
+			// @time returns the time, this is one of the simpler tests for
+			// testing if the bot actually works
+			case "@time":
+				response := fmt.Sprintf("Here's the time: %s", time.Now().Format(time.UnixDate))
+				err = matrix.Send(authinfo, event.RoomName, response)
+				if err != nil {
+					fmt.Println("ERR @time")
+					fmt.Println(err)
+					return
+				}
+
+			// @report generates a wind report for the next 48 hours
+			case "@wind":
+				data := remote()
+				report := genReport(authinfo, data, "wind")
+				err = matrix.SendImage(authinfo, event.RoomName, report)
+				if err != nil {
+					fmt.Println("ERR @wind")
+					fmt.Println(err)
+					return
+				}
+
+			// @temp generates a temperature report for the next 48 hours
+			case "@temp":
+				data := remote()
+				report := genReport(authinfo, data, "temp")
+				err = matrix.SendImage(authinfo, event.RoomName, report)
+				if err != nil {
+					fmt.Println("ERR @temp")
+					fmt.Println(err)
+					return
+				}
+
+			// @pressure generates a pressure report for the next 48 hours
+			case "@pressure":
+				data := remote()
+				report := genReport(authinfo, data, "pressure")
+				err = matrix.SendImage(authinfo, event.RoomName, report)
+				if err != nil {
+					fmt.Println("ERR @pressure")
+					fmt.Println(err)
+					return
+				}
+
+			// @all generates an overall report for the next 48 hours containing wind, temperature and pressure plots
+			case "@all":
+				data := remote()
+
+				report := genReport(authinfo, data, "wind")
+				err = matrix.SendImage(authinfo, event.RoomName, report)
+				if err != nil {
+					fmt.Println("ERR @pressure")
+					fmt.Println(err)
+					return
+				}
+
+				report = genReport(authinfo, data, "temp")
+				err = matrix.SendImage(authinfo, event.RoomName, report)
+				if err != nil {
+					fmt.Println("ERR @pressure")
+					fmt.Println(err)
+					return
+				}
+
+				report = genReport(authinfo, data, "pressure")
+				err = matrix.SendImage(authinfo, event.RoomName, report)
+				if err != nil {
+					fmt.Println("ERR @pressure")
+					fmt.Println(err)
+					return
+				}
+
+			// in case of no message being present in the event, do nothing
+			case "":
+				break
+
+			// if anything other than the sutff above is sent, handle that as an error
+			default:
+				err = matrix.Send(authinfo, event.RoomName, "I couldn't understand that, could your repeat it more clearly?")
+				if err != nil {
+					fmt.Println("ERR DEFAULT")
+					fmt.Println(err)
+					return
+				}
+			}
+		}
+	}
+}
+
+// local uses a local "weather.json" file as the data, this reduces the amount
+// of requests done against the api during testing
+func local() onecallResponse {
+	dat, err := ioutil.ReadFile("weather.json")
+	if err != nil {
+		fmt.Println(err)
+		return onecallResponse{}
+	}
+
+	var data onecallResponse
+	err = json.Unmarshal(dat, &data)
+	if err != nil {
+		fmt.Printf("Could not unmarshal the data: %s", err)
+		return onecallResponse{}
+	}
+
+	return data
+}
+
+// remote fetches the information from the remote server
+func remote() onecallResponse {
+	// these values should be provided by the user
+	apikey := viper.GetString("openweathermap.apikey")
+	lat := viper.GetString("openweathermap.lat")
+	lon := viper.GetString("openweathermap.lon")
+
+	cli := gentleman.New()
+	cli.URL("https://api.openweathermap.org")
+
+	req := cli.Request()
+	req.Path("/data/2.5/onecall")
+
+	cli.Use(query.Set("lat", lat))
+	cli.Use(query.Set("lon", lon))
+	cli.Use(query.Set("appid", apikey))
+	cli.Use(query.Set("units", "metric"))
+
+	// Perform the request
+	res, err := req.Send()
+	if err != nil {
+		fmt.Printf("Request error: %s\n", err)
+		return onecallResponse{}
+	}
+	if !res.Ok {
+		fmt.Printf("Invalid server response: %d\n", res.StatusCode)
+		return onecallResponse{}
+	}
+
+	// unmarshal the request to into a onecallResponse struct
+	var data onecallResponse
+	err = json.Unmarshal(res.Bytes(), &data)
+	if err != nil {
+		fmt.Printf("Could not unmarshal the data: %s", err)
+		return onecallResponse{}
+	}
+
+	return data
+}
+
+// genReport generates the report matching the given report type, being either
+// "wind", "temp" or "pressure" returning a map defining the image in a way
+// matrix can process (just look at the code)
+func genReport(authinfo matrix.Authinfo, data onecallResponse, reportType string) map[string]interface{} {
+
+	var windValues []float64
+	var tempValues []float64
+	var pressureValues []float64
+
+	var title string // the title displayed above the plot
+	var name string  // the name of the chart
+
+	var yValues []float64
+
+	// fill the slices with the correct value
+	switch reportType {
+	case "wind":
+		for _, element := range data.Hourly {
+			windValues = append(windValues, element.WindSpeed*2)
+		}
+		title = "Wind (kt) - Next 48h"
+		name = "Wind speed (kt)"
+		yValues = windValues
+
+	case "temp":
+		for _, element := range data.Hourly {
+			tempValues = append(tempValues, element.Temp)
+		}
+		title = "Temperature ⁰C - Next 48h"
+		name = "Temperature (⁰C)"
+		yValues = tempValues
+
+	case "pressure":
+		for _, element := range data.Hourly {
+			pressureValues = append(pressureValues, float64(element.Pressure))
+		}
+		title = "Pressure hPa - Next 48h"
+		name = "Pressure (hPa)"
+		yValues = pressureValues
+
+	}
+
+	// generate the xValues
+	var xValues []time.Time
+	for i := 1; i < 48; i++ {
+		xValues = append(xValues, time.Now().Add(time.Duration(i)*time.Hour))
+	}
+
+	// define the plot
+	graph := chart.Chart{
+		Title: title,
+		TitleStyle: chart.Style{
+			Show: true,
+		},
+		Width:  2000,
+		Height: 500,
+		Series: []chart.Series{
+			chart.TimeSeries{
+				Name: name,
+				Style: chart.Style{
+					Show:        true,
+					StrokeColor: chart.GetDefaultColor(0),
+					FillColor:   chart.GetDefaultColor(0),
+				},
+				XValues: xValues,
+				YValues: yValues,
+			},
+		},
+		XAxis: chart.XAxis{
+			Style: chart.Style{
+				Show: true,
+			},
+			ValueFormatter: chart.TimeHourValueFormatter,
+			Name:           "Time",
+		},
+		YAxis: chart.YAxis{
+			Style: chart.Style{
+				Show: true,
+			},
+			Name: name,
+		},
+	}
+
+	// render the image
+	buffer := bytes.NewBuffer([]byte{})
+	err := graph.Render(chart.PNG, buffer)
+	if err != nil {
+		fmt.Println(err)
+		return map[string]interface{}{}
+	}
+
+	// write the plot to a file
+	// ioutil.WriteFile("plot.png", buffer.Bytes(), 0644)
+
+	// upload the image to homeserver
+	mxcID, err := matrix.Upload(authinfo, "plot.png", buffer)
+	if err != nil {
+		fmt.Println(err)
+		return map[string]interface{}{}
+	}
+
+	image := map[string]interface{}{
+		"msgtype": "m.image",
+		"url":     mxcID.ContentURI,
+		"info": map[string]interface{}{
+			"h":        graph.GetHeight(),
+			"w":        graph.GetWidth(),
+			"mimetype": "image/jpeg",
+			"size":     len(buffer.Bytes()),
+		},
+		"body": "A",
+	}
+
+	return image
+}
+
+// initConfig intializes the config
+func initConfig() {
+	viper.SetConfigName("config")
+	viper.AddConfigPath(".")
+	viper.AutomaticEnv()
+
+	err := viper.ReadInConfig()
+	if err != nil {
+		panic(fmt.Errorf("Fatal error reading the config file: %s", err))
+	}
+}