From 7f05fbcda582a9da9885437dde816c2304933300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Frange=C5=BE?= Date: Sat, 3 Oct 2020 23:39:23 +0200 Subject: [PATCH] Implemented UptimeRobot widget This is the first working version of the UptimeRobot module, as discussed in #979 --- app/widget_maker.go | 4 + modules/uptimerobot/keyboard.go | 6 ++ modules/uptimerobot/settings.go | 35 +++++++ modules/uptimerobot/widget.go | 157 ++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 modules/uptimerobot/keyboard.go create mode 100644 modules/uptimerobot/settings.go create mode 100644 modules/uptimerobot/widget.go diff --git a/app/widget_maker.go b/app/widget_maker.go index 6f7275a8..e787e2a2 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -66,6 +66,7 @@ import ( "github.com/wtfutil/wtf/modules/twitter" "github.com/wtfutil/wtf/modules/twitterstats" "github.com/wtfutil/wtf/modules/unknown" + "github.com/wtfutil/wtf/modules/uptimerobot" "github.com/wtfutil/wtf/modules/victorops" "github.com/wtfutil/wtf/modules/weatherservices/arpansagovau" "github.com/wtfutil/wtf/modules/weatherservices/prettyweather" @@ -292,6 +293,9 @@ func MakeWidget( case "twitterstats": settings := twitterstats.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = twitterstats.NewWidget(app, pages, settings) + case "uptimerobot": + settings := uptimerobot.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = uptimerobot.NewWidget(app, pages, settings) case "victorops": settings := victorops.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = victorops.NewWidget(app, settings) diff --git a/modules/uptimerobot/keyboard.go b/modules/uptimerobot/keyboard.go new file mode 100644 index 00000000..483a4ef2 --- /dev/null +++ b/modules/uptimerobot/keyboard.go @@ -0,0 +1,6 @@ +package uptimerobot + +func (widget *Widget) initializeKeyboardControls() { + widget.SetKeyboardChar("/", widget.ShowHelp, "Show/hide this help widget") + widget.SetKeyboardChar("r", widget.Refresh, "Refresh widget") +} diff --git a/modules/uptimerobot/settings.go b/modules/uptimerobot/settings.go new file mode 100644 index 00000000..fe638ee1 --- /dev/null +++ b/modules/uptimerobot/settings.go @@ -0,0 +1,35 @@ +package uptimerobot + +import ( + "os" + + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = true + defaultTitle = "Uptime Robot" +) + +type Settings struct { + common *cfg.Common + + apiKey string `help:"An UptimeRobot API key."` + uptimePeriods string `help:"The periods over which to display uptime (in days, dash-separated)." optional:"true"` +} + +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + + apiKey: ymlConfig.UString("apiKey", os.Getenv("WTF_UPTIMEROBOT_APIKEY")), + uptimePeriods: ymlConfig.UString("uptimePeriods", "30"), + } + + cfg.ModuleSecret(name, globalConfig, &settings.apiKey). + Service("https://api.uptimerobot.com").Load() + + return &settings +} diff --git a/modules/uptimerobot/widget.go b/modules/uptimerobot/widget.go new file mode 100644 index 00000000..f9917b5e --- /dev/null +++ b/modules/uptimerobot/widget.go @@ -0,0 +1,157 @@ +package uptimerobot + +import ( + "fmt" + "errors" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" +) + +type Widget struct { + view.KeyboardWidget + view.ScrollableWidget + + monitors []Monitor + settings *Settings + err error +} + +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + widget := &Widget{ + KeyboardWidget: view.NewKeyboardWidget(app, pages, settings.common), + ScrollableWidget: view.NewScrollableWidget(app, settings.common), + + settings: settings, + } + + widget.SetRenderFunction(widget.Render) + widget.initializeKeyboardControls() + widget.View.SetInputCapture(widget.InputCapture) + + widget.KeyboardWidget.SetView(widget.View) + + return widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + + monitors, err := widget.getMonitors() + widget.monitors = monitors + widget.err = err + widget.SetItemCount(len(monitors)) + + widget.Render() +} + +// Render sets up the widget data for redrawing to the screen +func (widget *Widget) Render() { + widget.Redraw(widget.content) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) content() (string, string, bool) { + numUp := 0 + for _, monitor := range widget.monitors { + if monitor.State == 2 { + numUp++ + } + } + + title := fmt.Sprintf("UptimeRobot (%d/%d)", numUp, len(widget.monitors)) + + if widget.err != nil { + return title, widget.err.Error(), true + } + + if widget.monitors == nil { + return title, "No monitors to display", false + } + + str := widget.contentFrom(widget.monitors) + + return title, str, false +} + +func (widget *Widget) contentFrom(monitors []Monitor) string { + var str string + + for _, monitor := range monitors { + prefix := "" + + switch monitor.State { + case 2: + prefix += "[green] + " + break + case 8: + case 9: + prefix += "[red] - " + break + default: + prefix += "[yellow] ~ " + } + + str += fmt.Sprintf(`%s%s [gray](%s)[white] +`, + prefix, + monitor.Name, + monitor.Uptime, + ) + } + + return str +} + +type Monitor struct { + Name string `json:"friendly_name"` + // Monitor state, see: https://uptimerobot.com/api/#parameters + State int8 `json:"status"` + // Uptime ratio, preformatted, e.g.: 100.000-97.233-96.975 + Uptime string `json:"custom_uptime_ratio"` +} + +func (widget *Widget) getMonitors() ([]Monitor, error) { + // See: https://uptimerobot.com/api/#getMonitorsWrap + resp, err_h := http.PostForm("https://api.uptimerobot.com/v2/getMonitors", + url.Values{ + "api_key": {widget.settings.apiKey}, + "format": {"json"}, + "custom_uptime_ratios": {widget.settings.uptimePeriods}, + }, + ) + + if err_h != nil { + return nil, err_h + } + + body, _ := ioutil.ReadAll(resp.Body) + + // First pass to read the status + c := make(map[string]json.RawMessage) + json.Unmarshal([]byte(body), &c) + + stat := string(c["stat"]) + if stat != `"ok"` { + return nil, errors.New(string(body)) + } + + // Second pass to get the actual info + var monitors []Monitor + err_j := json.Unmarshal(c["monitors"], &monitors) + + if err_j != nil { + return nil, err_j + } + + return monitors, nil +}