From 8e9453304a9396dfd781cb710e8a63f5200c68ed Mon Sep 17 00:00:00 2001 From: hanemile Date: Fri, 10 Jul 2020 16:24:36 +0200 Subject: the main logic --- main.go | 348 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 main.go 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)) + } +} -- cgit 1.4.1