jlelse
/
kis3
Archived
1
Fork 0

Rework Telegram integration

This commit is contained in:
Jan-Lukas Else 2020-04-22 21:40:28 +02:00
parent 95952aae6b
commit 41e2766958
8 changed files with 184 additions and 80 deletions

View File

@ -32,6 +32,8 @@ You can configure some settings using a `config.json` file in the working direct
`port` (`8080`): Set the port to which KISSS should listen `port` (`8080`): Set the port to which KISSS should listen
`baseUrl` (optional, required for Telegram): Set the base URL on which KISSS runs
`dnt` (`true`): Set whether or not KISSS should respect Do-Not-Track headers some browsers send `dnt` (`true`): Set whether or not KISSS should respect Do-Not-Track headers some browsers send
`dbPath` (`data/kis3.db`): Set the path for the SQLite database (relative to the working directory - in the Docker container it's `/app`). `dbPath` (`data/kis3.db`): Set the path for the SQLite database (relative to the working directory - in the Docker container it's `/app`).
@ -50,7 +52,7 @@ 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`, `BASE_URL`, `DNT`, `DB_PATH`, `STATS_USERNAME`, `STATS_PASSWORD`), that will override the settings from the configuration file.
### Email ### Email
@ -68,7 +70,9 @@ To enable email integration for sending reports, you need to add some configurat
The Telegram integration allows sending reports via Telegram and also requesting stats via Telegram. For that the following configuration value must be set: 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). `tgBotToken`: Token for the Telegram bot, which you can request via the [Bot Father](https://t.me/BotFather)
`tgHookSecret` (optional): Secret, so nobody (except KISSS and Telegram) knows the URL to which Telegram should send updates about new messages
## Add to website ## Add to website

View File

@ -10,6 +10,7 @@ import (
type config struct { type config struct {
Port string `json:"port"` Port string `json:"port"`
BaseUrl string `json:"baseUrl"`
Dnt bool `json:"dnt"` Dnt bool `json:"dnt"`
DbPath string `json:"dbPath"` DbPath string `json:"dbPath"`
StatsUsername string `json:"statsUsername"` StatsUsername string `json:"statsUsername"`
@ -19,17 +20,15 @@ type config struct {
SmtpPassword string `json:"smtpPassword"` SmtpPassword string `json:"smtpPassword"`
SmtpHost string `json:"smtpHost"` SmtpHost string `json:"smtpHost"`
TGBotToken string `json:"tgBotToken"` TGBotToken string `json:"tgBotToken"`
TGHookSecret string `json:"tgHookSecret"`
Reports []report `json:"reports"` Reports []report `json:"reports"`
} }
var ( var (
appConfig = &config{ appConfig = &config{
Port: "8080", Port: "8080",
Dnt: true, Dnt: true,
DbPath: "data/kis3.db", DbPath: "data/kis3.db",
StatsUsername: "",
StatsPassword: "",
TGBotToken: "",
} }
) )
@ -58,6 +57,10 @@ func overwriteEnvVarValues(appConfig *config) {
if set { if set {
appConfig.Port = port appConfig.Port = port
} }
baseUrl, set := os.LookupEnv("BASE_URL")
if set {
appConfig.BaseUrl = baseUrl
}
dntString, set := os.LookupEnv("DNT") dntString, set := os.LookupEnv("DNT")
dntBool, e := strconv.ParseBool(dntString) dntBool, e := strconv.ParseBool(dntString)
if set && e == nil { if set && e == nil {

2
go.mod
View File

@ -4,7 +4,6 @@ go 1.14
require ( require (
github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/gobuffalo/packr/v2 v2.8.0 github.com/gobuffalo/packr/v2 v2.8.0
github.com/google/uuid v1.1.1 // indirect github.com/google/uuid v1.1.1 // indirect
github.com/gorilla/handlers v1.4.2 github.com/gorilla/handlers v1.4.2
@ -18,7 +17,6 @@ require (
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/rubenv/sql-migrate v0.0.0-20200401080106-baf4e4b6812c github.com/rubenv/sql-migrate v0.0.0-20200401080106-baf4e4b6812c
github.com/sirupsen/logrus v1.5.0 // indirect github.com/sirupsen/logrus v1.5.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/whiteshtef/clockwork v0.0.0-20200221012748-027e62affd84 github.com/whiteshtef/clockwork v0.0.0-20200221012748-027e62affd84
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect

4
go.sum
View File

@ -36,8 +36,6 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
@ -180,8 +178,6 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=

21
main.go
View File

@ -8,7 +8,6 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
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"
) )
@ -16,7 +15,7 @@ import (
type kis3 struct { type kis3 struct {
router *mux.Router router *mux.Router
staticBox *packr.Box staticBox *packr.Box
tgBot *tgbotapi.BotAPI telegram *telegram
} }
var ( var (
@ -32,12 +31,11 @@ func main() {
if e != nil { if e != nil {
log.Fatal("Database setup failed:", e) log.Fatal("Database setup failed:", e)
} }
initTelegram()
initRouter() initRouter()
initTelegramBot()
// Start // Start
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)
@ -50,20 +48,9 @@ func initRouter() {
app.router = mux.NewRouter() app.router = mux.NewRouter()
initStatsRouter() initStatsRouter()
initTrackingRouter() initTrackingRouter()
} if app.telegram != nil {
initTelegramRouter()
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() {

View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/jordan-wright/email" "github.com/jordan-wright/email"
"github.com/whiteshtef/clockwork" "github.com/whiteshtef/clockwork"
"io/ioutil" "io/ioutil"
@ -17,7 +16,7 @@ type report struct {
Query string `json:"query"` Query string `json:"query"`
Type string `json:"type"` Type string `json:"type"`
To string `json:"to"` To string `json:"to"`
TGUserId int64 `json:"tgUserId"` TGUserId int `json:"tgUserId"`
} }
func startReports() { func startReports() {
@ -82,12 +81,11 @@ func sendMail(r *report, content []byte) {
} }
func sendTelegram(r *report, content []byte) { func sendTelegram(r *report, content []byte) {
if r.TGUserId == 0 || app.tgBot == nil { if r.TGUserId == 0 || app.telegram == nil {
fmt.Println("No valid report configuration") fmt.Println("No valid report configuration")
return return
} }
msg := tgbotapi.NewMessage(r.TGUserId, r.Name+"\n\n"+string(content)) e := app.telegram.sendMessage(r.TGUserId, r.Name+"\n\n"+string(content))
_, 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

@ -9,9 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"kis3.dev/kis3/helpers" "kis3.dev/kis3/helpers"
"github.com/whiteshtef/clockwork"
) )
func initStatsRouter() { func initStatsRouter() {
@ -134,54 +132,31 @@ func sendChartResponse(result []*RequestResultRow, w http.ResponseWriter) {
_ = t.Execute(w, data) _ = t.Execute(w, data)
} }
func startStatsTelegram() { func respondToTelegramUpdate(u *telegramUpdate) {
if app.tgBot == nil { if app.telegram != nil && strings.HasPrefix(u.Message.Text, "/stats") {
return response := ""
} fakeUrl, e := url.Parse("/stats?" + strings.TrimSpace(strings.TrimPrefix(u.Message.Text, "/stats")))
u := tgbotapi.NewUpdate(0) if e != nil {
scheduler := clockwork.NewScheduler() response = "Request failed"
scheduler.Schedule().Every(5).Seconds().Do(func() { } else {
checkForTelegramUpdates(&u) 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."
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 { } else {
if appConfig.statsAuth() && (fakeUrl.Query().Get("username") != appConfig.StatsUsername || fakeUrl.Query().Get("password") != appConfig.StatsPassword) { result, e := doRequest(fakeUrl.Query())
response = "Not authorized. Add username=yourusername&password=yourpassword to the query." if e != nil {
response = "Request failed"
} else { } else {
result, e := doRequest(fakeUrl.Query()) rowStrings := make([]string, len(result))
if e != nil { for i, row := range result {
response = "Request failed" rowStrings[i] = (*row).First + ": " + strconv.Itoa((*row).Second)
} else {
rowStrings := make([]string, len(result))
for i, row := range result {
rowStrings[i] = (*row).First + ": " + strconv.Itoa((*row).Second)
}
response = strings.Join(rowStrings, "\n")
} }
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 { e = app.telegram.replyToMessage(u.Message.Chat.Id, response, u.Message.Id)
u.Offset = update.UpdateID + 1 if e != nil {
fmt.Println("Failed to send message:", e)
} }
} }
} }

143
telegram.go Normal file
View File

@ -0,0 +1,143 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"strconv"
)
type telegram struct {
botToken string
}
type telegramUpdate struct {
Message struct {
Chat struct {
Id int `json:"id"`
} `json:"chat"`
Id int `json:"message_id"`
Text string `json:"text"`
} `json:"message"`
}
func initTelegram() {
if appConfig.TGBotToken == "" {
fmt.Println("Telegram not configured.")
return
}
tg := &telegram{
botToken: appConfig.TGBotToken,
}
username, err := tg.getBotUsername()
if err != nil {
fmt.Println("Failed to setup Telegram:", err)
return
}
err = tg.setTelegramHook()
if err != nil {
fmt.Println("Failed to setup Telegram webhook:", err)
return
}
fmt.Println("Authorized Telegram bot on account", username)
app.telegram = tg
}
func initTelegramRouter() {
app.router.HandleFunc(path.Join("/telegramHook", appConfig.TGHookSecret), TelegramHookHandler)
}
func TelegramHookHandler(w http.ResponseWriter, r *http.Request) {
tgUpdate := &telegramUpdate{}
err := json.NewDecoder(r.Body).Decode(tgUpdate)
if err != nil {
http.Error(w, "Failed to decode body", http.StatusBadRequest)
return
}
go respondToTelegramUpdate(tgUpdate)
return
}
var telegramBaseUrl = "https://api.telegram.org/bot"
func (t *telegram) getBotUsername() (string, error) {
tgUrl, err := url.Parse(telegramBaseUrl + t.botToken + "/getMe")
if err != nil {
return "", errors.New("failed to create Telegram request")
}
req, _ := http.NewRequest(http.MethodPost, tgUrl.String(), nil)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
return "", errors.New("failed to get Telegram bot info")
}
tgBotInfo := &struct {
Ok bool `json:"ok"`
Result struct {
Id int `json:"id"`
Username string `json:"username"`
} `json:"result"`
}{}
err = json.NewDecoder(resp.Body).Decode(tgBotInfo)
_ = resp.Body.Close()
if err != nil || !tgBotInfo.Ok {
return "", errors.New("failed to parse Telegram bot info")
}
// If getMe returns no username, but only an ID for whatever reason
if len(tgBotInfo.Result.Username) == 0 {
tgBotInfo.Result.Username = strconv.Itoa(tgBotInfo.Result.Id)
}
return tgBotInfo.Result.Username, nil
}
func (t *telegram) setTelegramHook() error {
if len(appConfig.BaseUrl) < 1 {
return errors.New("base URL not configured")
}
hookUrl, e := url.Parse(appConfig.BaseUrl)
if e != nil {
return errors.New("failed to parse base URL")
}
hookUrl.Path = path.Join(hookUrl.Path, path.Join("telegramHook", appConfig.TGHookSecret))
params := url.Values{}
params.Add("url", hookUrl.String())
params.Add("allowed_updates", "[\"message\"]")
tgUrl, err := url.Parse(telegramBaseUrl + t.botToken + "/setWebhook")
if err != nil {
return errors.New("failed to create Telegram request")
}
tgUrl.RawQuery = params.Encode()
req, _ := http.NewRequest(http.MethodPost, tgUrl.String(), nil)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
return errors.New("failed to set Telegram webhook")
}
fmt.Println("Telegram webhook URL:", hookUrl.String())
return nil
}
func (t *telegram) sendMessage(chat int, message string) error {
return t.replyToMessage(chat, message, 0)
}
func (t *telegram) replyToMessage(chat int, message string, replyTo int) error {
params := url.Values{}
params.Add("chat_id", strconv.Itoa(chat))
if replyTo != 0 {
params.Add("reply_to_message_id", strconv.Itoa(replyTo))
}
params.Add("text", message)
tgUrl, err := url.Parse(telegramBaseUrl + t.botToken + "/sendMessage")
if err != nil {
return errors.New("failed to create Telegram request")
}
tgUrl.RawQuery = params.Encode()
req, _ := http.NewRequest(http.MethodPost, tgUrl.String(), nil)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
return errors.New("failed to send Telegram message")
}
return nil
}