Browse Source

Rework Telegram integration

master
Jan-Lukas Else 1 year ago
parent
commit
41e2766958
  1. 8
      README.md
  2. 15
      config.go
  3. 2
      go.mod
  4. 4
      go.sum
  5. 21
      main.go
  6. 8
      reports.go
  7. 63
      stats.go
  8. 143
      telegram.go

8
README.md

@ -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
`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
`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
@ -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:
`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

15
config.go

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

2
go.mod

@ -4,7 +4,6 @@ go 1.14
require (
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/google/uuid v1.1.1 // indirect
github.com/gorilla/handlers v1.4.2
@ -18,7 +17,6 @@ require (
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/rubenv/sql-migrate v0.0.0-20200401080106-baf4e4b6812c
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
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect

4
go.sum

@ -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/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
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.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
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.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
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/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=

21
main.go

@ -8,7 +8,6 @@ import (
"os/signal"
"syscall"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux"
)
@ -16,7 +15,7 @@ import (
type kis3 struct {
router *mux.Router
staticBox *packr.Box
tgBot *tgbotapi.BotAPI
telegram *telegram
}
var (
@ -32,12 +31,11 @@ func main() {
if e != nil {
log.Fatal("Database setup failed:", e)
}
initTelegram()
initRouter()
initTelegramBot()
// Start
go startListeningToWeb()
go startReports()
go startStatsTelegram()
// Graceful stop
var gracefulStop = make(chan os.Signal, 1)
signal.Notify(gracefulStop, os.Interrupt, syscall.SIGTERM)
@ -50,20 +48,9 @@ func initRouter() {
app.router = mux.NewRouter()
initStatsRouter()
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
if app.telegram != nil {
initTelegramRouter()
}
fmt.Println("Authorized Telegram bot on account", bot.Self.UserName)
app.tgBot = bot
}
func startListeningToWeb() {

8
reports.go

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

63
stats.go

@ -9,9 +9,7 @@ import (
"strconv"
"strings"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"kis3.dev/kis3/helpers"
"github.com/whiteshtef/clockwork"
)
func initStatsRouter() {
@ -134,54 +132,31 @@ 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"
func respondToTelegramUpdate(u *telegramUpdate) {
if app.telegram != nil && strings.HasPrefix(u.Message.Text, "/stats") {
response := ""
fakeUrl, e := url.Parse("/stats?" + strings.TrimSpace(strings.TrimPrefix(u.Message.Text, "/stats")))
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 {
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."
result, e := doRequest(fakeUrl.Query())
if e != nil {
response = "Request failed"
} 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")
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
e = app.telegram.replyToMessage(u.Message.Chat.Id, response, u.Message.Id)
if e != nil {
fmt.Println("Failed to send message:", e)
}
}
}

143
telegram.go

@ -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
}
Loading…
Cancel
Save