From c9a111c832d2cb9d0b1b8560811d66341695773e Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Tue, 31 Oct 2017 22:50:14 +0500 Subject: [PATCH] Telegram pusher. --- config/struct/struct.go | 39 ++++--- context/context.go | 98 ++++++++-------- opensaps.example.yaml | 12 ++ opensaps.go | 76 ++++++------ pushers/telegram/exported.go | 37 ++++++ pushers/telegram/telegramconnection.go | 156 +++++++++++++++++++++++++ pushers/telegram/telegrampusher.go | 56 +++++++++ 7 files changed, 372 insertions(+), 102 deletions(-) create mode 100644 pushers/telegram/exported.go create mode 100644 pushers/telegram/telegramconnection.go create mode 100644 pushers/telegram/telegrampusher.go diff --git a/config/struct/struct.go b/config/struct/struct.go index 12528f5..b366199 100644 --- a/config/struct/struct.go +++ b/config/struct/struct.go @@ -19,41 +19,48 @@ package configstruct // Config's root. type ConfigStruct struct { - SlackHandler ConfigSlackHandler `yaml:"slackhandler"` - Webhooks map[string]ConfigWebhook `yaml:"webhooks"` - Matrix map[string]ConfigMatrix `yaml:"matrix"` + SlackHandler ConfigSlackHandler `yaml:"slackhandler"` + Webhooks map[string]ConfigWebhook `yaml:"webhooks"` + Matrix map[string]ConfigMatrix `yaml:"matrix"` + Telegram map[string]ConfigTelegram `yaml:"telegram"` } // Slack handler configuration. type ConfigSlackHandler struct { - Listener ConfigSlackHandlerListener `yaml:"listener"` + Listener ConfigSlackHandlerListener `yaml:"listener"` } type ConfigSlackHandlerListener struct { - Address string `yaml:"address"` + Address string `yaml:"address"` } // Webhook configuration. type ConfigWebhook struct { - Slack ConfigWebhookSlack `yaml:"slack"` - Remote ConfigWebhookRemote `yaml:"remote"` + Slack ConfigWebhookSlack `yaml:"slack"` + Remote ConfigWebhookRemote `yaml:"remote"` } type ConfigWebhookSlack struct { - Random1 string `yaml:"random1"` - Random2 string `yaml:"random2"` - LongRandom string `yaml:"longrandom"` + Random1 string `yaml:"random1"` + Random2 string `yaml:"random2"` + LongRandom string `yaml:"longrandom"` } type ConfigWebhookRemote struct { - Pusher string `yaml:"pusher"` - PushTo string `yaml:"push_to"` + Pusher string `yaml:"pusher"` + PushTo string `yaml:"push_to"` } // Matrix pusher configuration. type ConfigMatrix struct { - ApiRoot string `yaml:"api_root"` - User string `yaml:"user"` - Password string `yaml:"password"` - Room string `yaml:"room"` + ApiRoot string `yaml:"api_root"` + User string `yaml:"user"` + Password string `yaml:"password"` + Room string `yaml:"room"` +} + +// Telegram pusher configuration +type ConfigTelegram struct { + BotID string `yaml:"bot_id"` + ChatID string `yaml:"chat_id"` } diff --git a/context/context.go b/context/context.go index 866f7a5..b31a464 100644 --- a/context/context.go +++ b/context/context.go @@ -18,93 +18,93 @@ package context import ( - // stdlib - "os" - "strings" + // stdlib + "os" + "strings" - // local - "lab.pztrn.name/pztrn/opensaps/config/interface" - "lab.pztrn.name/pztrn/opensaps/parsers/interface" - "lab.pztrn.name/pztrn/opensaps/pushers/interface" - "lab.pztrn.name/pztrn/opensaps/slack/apiserverinterface" - "lab.pztrn.name/pztrn/opensaps/slack/message" + // local + "lab.pztrn.name/pztrn/opensaps/config/interface" + "lab.pztrn.name/pztrn/opensaps/parsers/interface" + "lab.pztrn.name/pztrn/opensaps/pushers/interface" + "lab.pztrn.name/pztrn/opensaps/slack/apiserverinterface" + "lab.pztrn.name/pztrn/opensaps/slack/message" - // other - "lab.pztrn.name/golibs/flagger" - "lab.pztrn.name/golibs/mogrus" + // other + "lab.pztrn.name/golibs/flagger" + "lab.pztrn.name/golibs/mogrus" ) type Context struct { - Config configurationinterface.ConfigurationInterface - Flagger *flagger.Flagger - Log *mogrus.LoggerHandler - Parsers map[string]parserinterface.ParserInterface - Pushers map[string]pusherinterface.PusherInterface - SlackAPIServer slackapiserverinterface.SlackAPIServerInterface + Config configurationinterface.ConfigurationInterface + Flagger *flagger.Flagger + Log *mogrus.LoggerHandler + Parsers map[string]parserinterface.ParserInterface + Pushers map[string]pusherinterface.PusherInterface + SlackAPIServer slackapiserverinterface.SlackAPIServerInterface } func (c *Context) Initialize() { - c.Parsers = make(map[string]parserinterface.ParserInterface) - c.Pushers = make(map[string]pusherinterface.PusherInterface) + c.Parsers = make(map[string]parserinterface.ParserInterface) + c.Pushers = make(map[string]pusherinterface.PusherInterface) - l := mogrus.New() - l.Initialize() - c.Log = l.CreateLogger("opensaps") - c.Log.CreateOutput("stdout", os.Stdout, true) + l := mogrus.New() + l.Initialize() + c.Log = l.CreateLogger("opensaps") + c.Log.CreateOutput("stdout", os.Stdout, true) - c.Flagger = flagger.New(c.Log) - c.Flagger.Initialize() + c.Flagger = flagger.New(c.Log) + c.Flagger.Initialize() } // Registers configuration interface. func (c *Context) RegisterConfigurationInterface(ci configurationinterface.ConfigurationInterface) { - c.Config = ci - c.Config.Initialize() + c.Config = ci + c.Config.Initialize() } // Registers parser interface. func (c *Context) RegisterParserInterface(name string, iface parserinterface.ParserInterface) { - c.Parsers[name] = iface - c.Parsers[name].Initialize() + c.Parsers[name] = iface + c.Parsers[name].Initialize() } // Registers Pusher interface. func (c *Context) RegisterPusherInterface(name string, iface pusherinterface.PusherInterface) { - c.Pushers[name] = iface - c.Pushers[name].Initialize() + c.Pushers[name] = iface + c.Pushers[name].Initialize() } // Registers Slack API HTTP server control structure. // Russians will have pretty good luff on variable name. func (c *Context) RegisterSlackAPIServerInterface(sasi slackapiserverinterface.SlackAPIServerInterface) { - c.SlackAPIServer = sasi - c.SlackAPIServer.Initialize() + c.SlackAPIServer = sasi + c.SlackAPIServer.Initialize() } func (c *Context) SendToParser(name string, message slackmessage.SlackMessage) map[string]string { - parser, found := c.Parsers[strings.ToLower(name)] - if !found { - c.Log.Errorf("Parser '%s' not found, will use default one!", name) - return c.Parsers["default"].ParseMessage(message) - } + parser, found := c.Parsers[strings.ToLower(name)] + if !found { + c.Log.Errorf("Parser '%s' not found, will use default one!", name) + return c.Parsers["default"].ParseMessage(message) + } - return parser.ParseMessage(message) + return parser.ParseMessage(message) } func (c *Context) SendToPusher(protocol string, connection string, data slackmessage.SlackMessage) { - pusher, ok := c.Pushers[protocol] - if !ok { - c.Log.Errorf("Pusher not found (or initialized) for protocol '%s'!", protocol) - } + pusher, ok := c.Pushers[protocol] + if !ok { + c.Log.Errorf("Pusher not found (or initialized) for protocol '%s'!", protocol) + } - pusher.Push(connection, data) + pusher.Push(connection, data) } // Shutdown everything. func (c *Context) Shutdown() { - c.SlackAPIServer.Shutdown() + c.SlackAPIServer.Shutdown() - for _, pusher := range c.Pushers { - pusher.Shutdown() - } + for _, pusher := range c.Pushers { + pusher.Shutdown() + } } diff --git a/opensaps.example.yaml b/opensaps.example.yaml index 3d79df0..775144b 100644 --- a/opensaps.example.yaml +++ b/opensaps.example.yaml @@ -10,9 +10,21 @@ webhooks: remote: pusher: "matrix" push_to: "matrix_test" + gitea_to_telegram: + slack: + random1: "87654321" + random2: "12345678" + longrandom: "432109876543210987654321" + remote: + pusher: "telegram" + push_to: "telegram_test" matrix: matrix_test: api_root: "https://localhost:8448/_matrix/client/r0" user: "" password: "" room: "!roomid:server.tld" +telegram: + telegram_test: + bot_id: "bot:id" + chat_id: "chat_id or -chat_id" \ No newline at end of file diff --git a/opensaps.go b/opensaps.go index ab85e16..3990c38 100644 --- a/opensaps.go +++ b/opensaps.go @@ -18,53 +18,55 @@ package main import ( - // stdlib - "os" - "os/signal" - "syscall" + // stdlib + "os" + "os/signal" + "syscall" - // local - "lab.pztrn.name/pztrn/opensaps/config" - "lab.pztrn.name/pztrn/opensaps/context" - "lab.pztrn.name/pztrn/opensaps/parsers/default" - "lab.pztrn.name/pztrn/opensaps/parsers/gitea" - "lab.pztrn.name/pztrn/opensaps/parsers/gitlab" - "lab.pztrn.name/pztrn/opensaps/pushers/matrix" - "lab.pztrn.name/pztrn/opensaps/slack" + // local + "lab.pztrn.name/pztrn/opensaps/config" + "lab.pztrn.name/pztrn/opensaps/context" + "lab.pztrn.name/pztrn/opensaps/parsers/default" + "lab.pztrn.name/pztrn/opensaps/parsers/gitea" + "lab.pztrn.name/pztrn/opensaps/parsers/gitlab" + "lab.pztrn.name/pztrn/opensaps/pushers/matrix" + "lab.pztrn.name/pztrn/opensaps/pushers/telegram" + "lab.pztrn.name/pztrn/opensaps/slack" ) func main() { - c := context.New() - c.Initialize() + c := context.New() + c.Initialize() - config.New(c) + config.New(c) - c.Log.Infoln("Launching OpenSAPS...") + c.Log.Infoln("Launching OpenSAPS...") - c.Flagger.Parse() - c.Config.InitializeLater() - c.Config.LoadConfigurationFromFile() + c.Flagger.Parse() + c.Config.InitializeLater() + c.Config.LoadConfigurationFromFile() - slack.New(c) + slack.New(c) - // Initialize parsers. - defaultparser.New(c) - giteaparser.New(c) - gitlabparser.New(c) + // Initialize parsers. + defaultparser.New(c) + giteaparser.New(c) + gitlabparser.New(c) - // Initialize pushers. - matrixpusher.New(c) + // Initialize pushers. + matrixpusher.New(c) + telegrampusher.New(c) - // CTRL+C handler. - signal_handler := make(chan os.Signal, 1) - shutdown_done := make(chan bool, 1) - signal.Notify(signal_handler, os.Interrupt, syscall.SIGTERM) - go func() { - <-signal_handler - c.Shutdown() - shutdown_done <- true - }() + // CTRL+C handler. + signal_handler := make(chan os.Signal, 1) + shutdown_done := make(chan bool, 1) + signal.Notify(signal_handler, os.Interrupt, syscall.SIGTERM) + go func() { + <-signal_handler + c.Shutdown() + shutdown_done <- true + }() - <-shutdown_done - os.Exit(0) + <-shutdown_done + os.Exit(0) } diff --git a/pushers/telegram/exported.go b/pushers/telegram/exported.go new file mode 100644 index 0000000..cc01dfa --- /dev/null +++ b/pushers/telegram/exported.go @@ -0,0 +1,37 @@ +// OpenSAPS - Open Slack API server for everyone. +// +// Copyright (c) 2017, Stanislav N. aka pztrn. +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package telegrampusher + +import ( + // local + "lab.pztrn.name/pztrn/opensaps/context" + "lab.pztrn.name/pztrn/opensaps/pushers/interface" +) + +var ( + c *context.Context + connections map[string]*TelegramConnection +) + +func New(cc *context.Context) { + c = cc + connections = make(map[string]*TelegramConnection) + + tp := TelegramPusher{} + c.RegisterPusherInterface("telegram", pusherinterface.PusherInterface(tp)) +} diff --git a/pushers/telegram/telegramconnection.go b/pushers/telegram/telegramconnection.go new file mode 100644 index 0000000..39ae2d5 --- /dev/null +++ b/pushers/telegram/telegramconnection.go @@ -0,0 +1,156 @@ +// OpenSAPS - Open Slack API server for everyone. +// +// Copyright (c) 2017, Stanislav N. aka pztrn. +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package telegrampusher + +import ( + // stdlib + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + // local + "lab.pztrn.name/pztrn/opensaps/slack/message" +) + +type TelegramConnection struct { + botId string + chatId string + connName string +} + +func (tc *TelegramConnection) Initialize(connName string, botId string, chatId string) { + tc.connName = connName + tc.chatId = chatId + tc.botId = botId +} + +func (tc *TelegramConnection) ProcessMessage(message slackmessage.SlackMessage) { + // Prepare message body. + message_data := c.SendToParser(message.Username, message) + + // Get message template. + msg_tpl := message_data["message"] + delete(message_data, "message") + + // Repeatables. + var repeatables []string + repeatables_raw, repeatables_found := message_data["repeatables"] + if repeatables_found { + repeatables = strings.Split(repeatables_raw, ",") + c.Log.Debugln("Repeatable keys:", repeatables, ", length:", len(repeatables)) + } + + // Process keys. + for key, value := range message_data { + // Do nothing for keys with "_url" appendix. + if strings.Contains(key, "_url") { + c.Log.Debugln("_url key found in pre-stage, skipping:", key) + continue + } + // Do nothing (yet) on repeatables. + if strings.Contains(key, "repeatable") { + c.Log.Debugln("Key containing 'repeatable' in pre-stage, skipping:", key) + continue + } + + if len(repeatables) > 0 { + if strings.Contains(key, "repeatable_item_") { + c.Log.Debugln("Repeatable key in pre-stage, skipping:", key) + continue + } + } + c.Log.Debugln("Processing message data key:", key) + + // Check if we have an item with "_url" appendix. This means + // that we should generate a link. + val_url, found := message_data[key+"_url"] + // Generate a link and put into message if key with "_url" + // was found. + var s string = "" + if found { + c.Log.Debugln("Found _url key, will create HTML link") + s = fmt.Sprintf("%s", val_url, value) + } else { + c.Log.Debugln("Found no _url key, will use as-is") + s = value + } + msg_tpl = strings.Replace(msg_tpl, "{"+key+"}", s, -1) + } + + // Process repeatables. + repeatable_tpl, repeatable_found := message_data["repeatable_message"] + if repeatable_found { + var repeatables_string string = "" + repeatables_count, _ := strconv.Atoi(message_data["repeatables_count"]) + idx := 0 + for { + if idx == repeatables_count { + c.Log.Debug("IDX goes above repeatables_count, breaking loop") + break + } + + var repstring string = repeatable_tpl + for i := range repeatables { + c.Log.Debugln("Processing repeatable variable:", repeatables[i]+strconv.Itoa(idx)) + var data string = "" + rdata := message_data["repeatable_item_"+repeatables[i]+strconv.Itoa(idx)] + rurl, rurl_found := message_data["repeatable_item_"+repeatables[i]+strconv.Itoa(idx)+"_url"] + if rurl_found { + c.Log.Debugln("Found _url key, will create HTML link") + data = fmt.Sprintf("%s", rurl, rdata) + } else { + c.Log.Debugln("Found no _url key, will use as-is") + data = rdata + } + repstring = strings.Replace(repstring, "{"+repeatables[i]+"}", data, -1) + } + + repeatables_string += repstring + c.Log.Debugln("Repeatable string:", repstring) + idx += 1 + } + + msg_tpl = strings.Replace(msg_tpl, "{repeatables}", repeatables_string, -1) + } + + msg_tpl = strings.Replace(msg_tpl, "{newline}", "\n", -1) + + c.Log.Debugln("Crafted message:", msg_tpl) + + // Send message. + tc.SendMessage(msg_tpl) +} + +func (tc *TelegramConnection) SendMessage(message string) { + msgdata := url.Values{} + msgdata.Set("chat_id", tc.chatId) + msgdata.Set("text", message) + msgdata.Set("parse_mode", "HTML") + + client := &http.Client{} + botUrl := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", tc.botId) + c.Log.Debugln("Bot URL:", botUrl) + response, _ := client.PostForm(botUrl, msgdata) + c.Log.Debugln("Status:", response.Status) +} + +func (tc *TelegramConnection) Shutdown() { + // There is nothing we can do actually. +} diff --git a/pushers/telegram/telegrampusher.go b/pushers/telegram/telegrampusher.go new file mode 100644 index 0000000..8495a32 --- /dev/null +++ b/pushers/telegram/telegrampusher.go @@ -0,0 +1,56 @@ +// OpenSAPS - Open Slack API server for everyone. +// +// Copyright (c) 2017, Stanislav N. aka pztrn. +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the itplied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package telegrampusher + +import ( + // local + "lab.pztrn.name/pztrn/opensaps/slack/message" +) + +type TelegramPusher struct{} + +func (tp TelegramPusher) Initialize() { + c.Log.Infoln("Initializing Telegram protocol pusher...") + + // Get configuration for pushers and initialize every connection. + cfg := c.Config.GetConfig() + for name, config := range cfg.Telegram { + c.Log.Infof("Initializing connection: '%s'", name) + conn := TelegramConnection{} + connections[name] = &conn + go conn.Initialize(name, config.BotID, config.ChatID) + } +} + +func (tp TelegramPusher) Push(connection string, data slackmessage.SlackMessage) { + conn, found := connections[connection] + if !found { + c.Log.Errorf("Connection not found: '%s'!", connection) + return + } + c.Log.Debugf("Pushing data to '%s'", connection) + conn.ProcessMessage(data) +} + +func (tp TelegramPusher) Shutdown() { + c.Log.Infoln("Shutting down Telegram pusher...") + + for _, conn := range connections { + conn.Shutdown() + } +}