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)) } }