From e9e25ae41ae9b0efefa8880119e68f4779ac5654 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Mon, 27 May 2019 15:03:30 +0200 Subject: [PATCH] Feature: Request stats via Telegram --- README.md | 47 +++++++++++++++++++++------ config.go | 6 ++++ main.go | 18 +++++++++++ reports.go | 34 +++++++------------- stats.go | 94 ++++++++++++++++++++++++++++++++++++++++++++---------- 5 files changed, 150 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 4c90bd5..3de013f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ KISSS is really easy to install via Docker. - docker run -d --name kis3 -p 8080:8080 -v kis3:/app/data -v ${pwd}/config.json:/app/config.json kis3/kis3 +```bash +docker run -d --name kis3 -p 8080:8080 -v kis3:/app/data -v ${pwd}/config.json:/app/config.json kis3/kis3 +``` Depending on your setup, replace `-p 8080:8080` with your custom port configuration. KISSS listens to port 8080 by default, but you can also change this via the configuration. @@ -18,9 +20,11 @@ You should also mount a configuration file to `/app/config.json`. It's also possible to use KISSS without Docker, but for that you need to compile it yourself. All you need to do so is installing go (follow the [instruction](https://golang.org/doc/install) or use [distro.tools](https://distro.tools) to install the latest version on Linux - you need at least version 1.12) and execute the following command: - go get -u github.com/kis3/kis3 - - After that there should be an executable with the name `kis3` in `$HOME/go/bin`. +```bash +go get -u github.com/kis3/kis3 +``` + +After that there should be an executable with the name `kis3` in `$HOME/go/bin`. ## Configuration @@ -48,6 +52,24 @@ The configuration file can look like this: If you specify an environment variable (`PORT`, `DNT`, `DB_PATH`, `STATS_USERNAME`, `STATS_PASSWORD`), that will override the settings from the configuration file. +### Email + +To enable email integration for sending reports, you need to add some configuration values for that: + +`smtpFrom`: Sender address for the emails + +`smtpHost`: Address of the mail server (including port) + +`smtpUser`: Username for SMTP login + +`smtpPassword`: Password for SMTP login + +### Telegram + +The Telegram integration allows sending reports via Telegram and also requesting stats via Telegram. For that the following configuration value must be set: + +`tgBotToken`: Token for the Telegram bot, which you can request via the [Bot Father](https://t.me/BotFather). + ## Add to website You can add the KISSS tracker to any website by putting `` just before `` in the HTML. Just replace `yourkis3domain.tld` with the correct address. @@ -80,6 +102,12 @@ The following filters are available: `format`: the format to represent the data, default is `plain` for a simple plain text list, `json` for a JSON response or `chart` for a chart generated with ChartJS in the browser +### Via Telegram + +You can also request stats via Telegram (in case you enable the Telegram integration). To do so, simply send a message with the command `stats` like `/stats view=pages...`. + +If you have authentication enabled, you need to add `username=yourusername&password=yourpassword` to the query. + ## Daily reports KISSS has a feature that can send you daily reports. It basically requests the statistics and sends the response via your preferred communication channel (mail or Telegram). You can configure it by adding report configurations to the configuration file: @@ -89,20 +117,19 @@ KISSS has a feature that can send you daily reports. It basically requests the s // Other configurations... "reports": [ { + // Email configuration "name": "Daily stats from KISSS", "time": "15:00", "query": "view=pages&ordercol=second&order=desc", "from": "myemailaddress@mydomain.tld", - "to": "myemailaddress@mydomain.tld", - "smtpHost": "mail.mydomain.tld:587", - "smtpUser": "myemailaddress@mydomain.tld", - "smtpPassword": "mysecretpassword" + "to": "myemailaddress@mydomain.tld" }, { + // Telegram configuration "name": "Daily stats from KISSS", + "type": "telegram", // Add this for Telegram "time": "15:00", "query": "view=pages&ordercol=second&order=desc", - "tgBotToken": "TelegramBotToken", "tgUserId": 123456 }, { @@ -112,7 +139,7 @@ KISSS has a feature that can send you daily reports. It basically requests the s } ``` -To use Telegram for reports, create a bot with the [Bot Father](https://t.me/BotFather) and request your user id from [@userinfobot](https://t.me/userinfobot). +You can find out your Telegram user id using [@userinfobot](https://t.me/userinfobot). ## License diff --git a/config.go b/config.go index 5ce6d6e..20c51af 100644 --- a/config.go +++ b/config.go @@ -14,6 +14,11 @@ type config struct { DbPath string `json:"dbPath"` StatsUsername string `json:"statsUsername"` StatsPassword string `json:"statsPassword"` + SmtpFrom string `json:"smtpfrom"` + SmtpUser string `json:"smtpUser"` + SmtpPassword string `json:"smtpPassword"` + SmtpHost string `json:"smtpHost"` + TGBotToken string `json:"tgBotToken"` Reports []report `json:"reports"` } @@ -24,6 +29,7 @@ var ( DbPath: "data/kis3.db", StatsUsername: "", StatsPassword: "", + TGBotToken: "", } ) diff --git a/main.go b/main.go index afb67bf..601f864 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/gobuffalo/packr/v2" "github.com/gorilla/mux" "log" @@ -14,6 +15,7 @@ import ( type kis3 struct { router *mux.Router staticBox *packr.Box + tgBot *tgbotapi.BotAPI } var ( @@ -29,11 +31,13 @@ func init() { log.Fatal("Database setup failed:", e) } initRouter() + initTelegramBot() } func main() { go startListeningToWeb() go startReports() + go startStatsTelegram() // Graceful stop var gracefulStop = make(chan os.Signal, 1) signal.Notify(gracefulStop, os.Interrupt, syscall.SIGTERM) @@ -48,6 +52,20 @@ func initRouter() { initTrackingRouter() } +func initTelegramBot() { + if appConfig.TGBotToken == "" { + fmt.Println("Telegram not configured.") + return + } + bot, e := tgbotapi.NewBotAPI(appConfig.TGBotToken) + if e != nil { + fmt.Println("Failed to setup Telegram:", e) + return + } + fmt.Println("Authorized Telegram bot on account", bot.Self.UserName) + app.tgBot = bot +} + func startListeningToWeb() { port := appConfig.Port addr := ":" + port diff --git a/reports.go b/reports.go index efd019b..8612521 100644 --- a/reports.go +++ b/reports.go @@ -12,17 +12,12 @@ import ( ) type report struct { - Name string `json:"name"` - Time string `json:"time"` - Query string `json:"query"` - Type string `json:"type"` - To string `json:"to"` - From string `json:"from"` - SmtpUser string `json:"smtpUser"` - SmtpPassword string `json:"smtpPassword"` - SmtpHost string `json:"smtpHost"` - TGBotToken string `json:"tgBotToken"` - TGUserId int64 `json:"tgUserId"` + Name string `json:"name"` + Time string `json:"time"` + Query string `json:"query"` + Type string `json:"type"` + To string `json:"to"` + TGUserId int64 `json:"tgUserId"` } func startReports() { @@ -67,17 +62,17 @@ func sendReport(r *report, content []byte) { } func sendMail(r *report, content []byte) { - if r.To == "" || r.From == "" || r.SmtpUser == "" || r.SmtpHost == "" { + if r.To == "" || appConfig.SmtpFrom == "" || appConfig.SmtpUser == "" || appConfig.SmtpHost == "" { fmt.Println("No valid report configuration") return } - smtpHostNoPort, _, _ := net.SplitHostPort(r.SmtpHost) + smtpHostNoPort, _, _ := net.SplitHostPort(appConfig.SmtpHost) mail := email.NewEmail() - mail.From = r.From + mail.From = appConfig.SmtpFrom mail.To = []string{r.To} mail.Subject = "KISSS report: " + r.Name mail.Text = content - e := mail.Send(r.SmtpHost, smtp.PlainAuth("", r.SmtpUser, r.SmtpPassword, smtpHostNoPort)) + e := mail.Send(appConfig.SmtpHost, smtp.PlainAuth("", appConfig.SmtpUser, appConfig.SmtpPassword, smtpHostNoPort)) if e != nil { fmt.Println("Sending report failed:", e) return @@ -87,17 +82,12 @@ func sendMail(r *report, content []byte) { } func sendTelegram(r *report, content []byte) { - if r.TGUserId == 0 || r.TGBotToken == "" { + if r.TGUserId == 0 || app.tgBot == nil { fmt.Println("No valid report configuration") return } - bot, e := tgbotapi.NewBotAPI(r.TGBotToken) - if e != nil { - fmt.Println("Sending report failed:", e) - return - } msg := tgbotapi.NewMessage(r.TGUserId, r.Name+"\n\n"+string(content)) - _, e = bot.Send(msg) + _, e := app.tgBot.Send(msg) if e != nil { fmt.Println("Sending report failed:", e) return diff --git a/stats.go b/stats.go index d5dca93..b880096 100644 --- a/stats.go +++ b/stats.go @@ -3,14 +3,17 @@ package main import ( "encoding/json" "fmt" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/kis3/kis3/helpers" + "github.com/whiteshtef/clockwork" "html/template" "net/http" + "net/url" "strconv" "strings" ) -func initStatsRouter() { +func initStatsRouter() { app.router.HandleFunc("/stats", StatsHandler) } @@ -22,7 +25,25 @@ func StatsHandler(w http.ResponseWriter, r *http.Request) { } } // Do request - queries := r.URL.Query() + queryValues := r.URL.Query() + result, e := doRequest(queryValues) + if e != nil { + fmt.Println("Database request failed:", e) + w.WriteHeader(500) + } else if result != nil { + w.Header().Set("Cache-Control", "max-age=0") + switch queryValues.Get("format") { + case "json": + sendJsonResponse(result, w) + case "chart": + sendChartResponse(result, w) + default: // "plain" + sendPlainResponse(result, w) + } + } +} + +func doRequest(queries url.Values) (result []*RequestResultRow, e error) { view := PAGES switch strings.ToLower(queries.Get("view")) { case "pages": @@ -48,7 +69,7 @@ func StatsHandler(w http.ResponseWriter, r *http.Request) { case "count": view = COUNT } - result, e := request(&ViewsRequest{ + result, e = request(&ViewsRequest{ view: view, from: queries.Get("from"), fromRel: queries.Get("fromrel"), @@ -61,20 +82,7 @@ func StatsHandler(w http.ResponseWriter, r *http.Request) { order: strings.ToUpper(queries.Get("order")), limit: queries.Get("limit"), }) - if e != nil { - fmt.Println("Database request failed:", e) - w.WriteHeader(500) - } else if result != nil { - w.Header().Set("Cache-Control", "max-age=0") - switch queries.Get("format") { - case "json": - sendJsonResponse(result, w) - case "chart": - sendChartResponse(result, w) - default: // "plain" - sendPlainResponse(result, w) - } - } + return } func sendPlainResponse(result []*RequestResultRow, w http.ResponseWriter) { @@ -120,3 +128,55 @@ func sendChartResponse(result []*RequestResultRow, w http.ResponseWriter) { } _ = t.Execute(w, data) } + +func startStatsTelegram() { + if app.tgBot == nil { + return + } + u := tgbotapi.NewUpdate(0) + scheduler := clockwork.NewScheduler() + scheduler.Schedule().Every(5).Seconds().Do(func() { + checkForTelegramUpdates(&u) + }) + scheduler.Run() +} + +func checkForTelegramUpdates(u *tgbotapi.UpdateConfig) { + updates, e := app.tgBot.GetUpdates(*u) + if e != nil { + return + } + for _, update := range updates { + if update.Message != nil && update.Message.Command() == "stats" { + response := "" + fakeUrl, e := url.Parse("/stats?" + update.Message.CommandArguments()) + if e != nil { + response = "Request failed" + } else { + if appConfig.statsAuth() && (fakeUrl.Query().Get("username") != appConfig.StatsUsername || fakeUrl.Query().Get("password") != appConfig.StatsPassword) { + response = "Not authorized. Add username=yourusername&password=yourpassword to the query." + } else { + result, e := doRequest(fakeUrl.Query()) + if e != nil { + response = "Request failed" + } else { + rowStrings := make([]string, len(result)) + for i, row := range result { + rowStrings[i] = (*row).First + ": " + strconv.Itoa((*row).Second) + } + response = strings.Join(rowStrings, "\n") + } + } + } + msg := tgbotapi.NewMessage(update.Message.Chat.ID, response) + msg.ReplyToMessageID = update.Message.MessageID + _, e = app.tgBot.Send(msg) + if e != nil { + fmt.Println("Failed to send message:", e) + } + } + if update.UpdateID >= u.Offset { + u.Offset = update.UpdateID + 1 + } + } +}