jlelse
/
kis3
Archived
1
Fork 0

Feature: Request stats via Telegram

This commit is contained in:
Jan-Lukas Else 2019-05-27 15:03:30 +02:00
parent 94251f1968
commit e9e25ae41a
5 changed files with 150 additions and 49 deletions

View File

@ -6,7 +6,9 @@
KISSS is really easy to install via Docker. 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. 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: 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 ```bash
go get -u github.com/kis3/kis3
After that there should be an executable with the name `kis3` in `$HOME/go/bin`. ```
After that there should be an executable with the name `kis3` in `$HOME/go/bin`.
## Configuration ## 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. 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 ## Add to website
You can add the KISSS tracker to any website by putting `<script src="https://yourkis3domain.tld/kis3.js"></script>` just before `</body>` in the HTML. Just replace `yourkis3domain.tld` with the correct address. You can add the KISSS tracker to any website by putting `<script src="https://yourkis3domain.tld/kis3.js"></script>` just before `</body>` 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 `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 ## 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: 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... // Other configurations...
"reports": [ "reports": [
{ {
// Email configuration
"name": "Daily stats from KISSS", "name": "Daily stats from KISSS",
"time": "15:00", "time": "15:00",
"query": "view=pages&ordercol=second&order=desc", "query": "view=pages&ordercol=second&order=desc",
"from": "myemailaddress@mydomain.tld", "from": "myemailaddress@mydomain.tld",
"to": "myemailaddress@mydomain.tld", "to": "myemailaddress@mydomain.tld"
"smtpHost": "mail.mydomain.tld:587",
"smtpUser": "myemailaddress@mydomain.tld",
"smtpPassword": "mysecretpassword"
}, },
{ {
// Telegram configuration
"name": "Daily stats from KISSS", "name": "Daily stats from KISSS",
"type": "telegram", // Add this for Telegram
"time": "15:00", "time": "15:00",
"query": "view=pages&ordercol=second&order=desc", "query": "view=pages&ordercol=second&order=desc",
"tgBotToken": "TelegramBotToken",
"tgUserId": 123456 "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 ## License

View File

@ -14,6 +14,11 @@ type config struct {
DbPath string `json:"dbPath"` DbPath string `json:"dbPath"`
StatsUsername string `json:"statsUsername"` StatsUsername string `json:"statsUsername"`
StatsPassword string `json:"statsPassword"` 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"` Reports []report `json:"reports"`
} }
@ -24,6 +29,7 @@ var (
DbPath: "data/kis3.db", DbPath: "data/kis3.db",
StatsUsername: "", StatsUsername: "",
StatsPassword: "", StatsPassword: "",
TGBotToken: "",
} }
) )

18
main.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/gobuffalo/packr/v2" "github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"log" "log"
@ -14,6 +15,7 @@ import (
type kis3 struct { type kis3 struct {
router *mux.Router router *mux.Router
staticBox *packr.Box staticBox *packr.Box
tgBot *tgbotapi.BotAPI
} }
var ( var (
@ -29,11 +31,13 @@ func init() {
log.Fatal("Database setup failed:", e) log.Fatal("Database setup failed:", e)
} }
initRouter() initRouter()
initTelegramBot()
} }
func main() { func main() {
go startListeningToWeb() go startListeningToWeb()
go startReports() go startReports()
go startStatsTelegram()
// Graceful stop // Graceful stop
var gracefulStop = make(chan os.Signal, 1) var gracefulStop = make(chan os.Signal, 1)
signal.Notify(gracefulStop, os.Interrupt, syscall.SIGTERM) signal.Notify(gracefulStop, os.Interrupt, syscall.SIGTERM)
@ -48,6 +52,20 @@ func initRouter() {
initTrackingRouter() 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() { func startListeningToWeb() {
port := appConfig.Port port := appConfig.Port
addr := ":" + port addr := ":" + port

View File

@ -12,17 +12,12 @@ import (
) )
type report struct { type report struct {
Name string `json:"name"` Name string `json:"name"`
Time string `json:"time"` Time string `json:"time"`
Query string `json:"query"` Query string `json:"query"`
Type string `json:"type"` Type string `json:"type"`
To string `json:"to"` To string `json:"to"`
From string `json:"from"` TGUserId int64 `json:"tgUserId"`
SmtpUser string `json:"smtpUser"`
SmtpPassword string `json:"smtpPassword"`
SmtpHost string `json:"smtpHost"`
TGBotToken string `json:"tgBotToken"`
TGUserId int64 `json:"tgUserId"`
} }
func startReports() { func startReports() {
@ -67,17 +62,17 @@ func sendReport(r *report, content []byte) {
} }
func sendMail(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") fmt.Println("No valid report configuration")
return return
} }
smtpHostNoPort, _, _ := net.SplitHostPort(r.SmtpHost) smtpHostNoPort, _, _ := net.SplitHostPort(appConfig.SmtpHost)
mail := email.NewEmail() mail := email.NewEmail()
mail.From = r.From mail.From = appConfig.SmtpFrom
mail.To = []string{r.To} mail.To = []string{r.To}
mail.Subject = "KISSS report: " + r.Name mail.Subject = "KISSS report: " + r.Name
mail.Text = content 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 { if e != nil {
fmt.Println("Sending report failed:", e) fmt.Println("Sending report failed:", e)
return return
@ -87,17 +82,12 @@ func sendMail(r *report, content []byte) {
} }
func sendTelegram(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") fmt.Println("No valid report configuration")
return 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)) msg := tgbotapi.NewMessage(r.TGUserId, r.Name+"\n\n"+string(content))
_, e = bot.Send(msg) _, e := app.tgBot.Send(msg)
if e != nil { if e != nil {
fmt.Println("Sending report failed:", e) fmt.Println("Sending report failed:", e)
return return

View File

@ -3,14 +3,17 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/kis3/kis3/helpers" "github.com/kis3/kis3/helpers"
"github.com/whiteshtef/clockwork"
"html/template" "html/template"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
) )
func initStatsRouter() { func initStatsRouter() {
app.router.HandleFunc("/stats", StatsHandler) app.router.HandleFunc("/stats", StatsHandler)
} }
@ -22,7 +25,25 @@ func StatsHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// Do 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 view := PAGES
switch strings.ToLower(queries.Get("view")) { switch strings.ToLower(queries.Get("view")) {
case "pages": case "pages":
@ -48,7 +69,7 @@ func StatsHandler(w http.ResponseWriter, r *http.Request) {
case "count": case "count":
view = COUNT view = COUNT
} }
result, e := request(&ViewsRequest{ result, e = request(&ViewsRequest{
view: view, view: view,
from: queries.Get("from"), from: queries.Get("from"),
fromRel: queries.Get("fromrel"), fromRel: queries.Get("fromrel"),
@ -61,20 +82,7 @@ func StatsHandler(w http.ResponseWriter, r *http.Request) {
order: strings.ToUpper(queries.Get("order")), order: strings.ToUpper(queries.Get("order")),
limit: queries.Get("limit"), limit: queries.Get("limit"),
}) })
if e != nil { return
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)
}
}
} }
func sendPlainResponse(result []*RequestResultRow, w http.ResponseWriter) { func sendPlainResponse(result []*RequestResultRow, w http.ResponseWriter) {
@ -120,3 +128,55 @@ func sendChartResponse(result []*RequestResultRow, w http.ResponseWriter) {
} }
_ = t.Execute(w, data) _ = 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
}
}
}