Rework Telegram integration
This commit is contained in:
parent
95952aae6b
commit
41e2766958
|
@ -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
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
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
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
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
|
||||
if app.telegram != nil {
|
||||
initTelegramRouter()
|
||||
}
|
||||
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() {
|
||||
|
|
|
@ -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
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Reference in New Issue