diff --git a/.github/workflows/build_on_tag.yml b/.github/workflows/build_on_tag_pr.yml similarity index 96% rename from .github/workflows/build_on_tag.yml rename to .github/workflows/build_on_tag_pr.yml index 2ff60bc..0c647e1 100644 --- a/.github/workflows/build_on_tag.yml +++ b/.github/workflows/build_on_tag_pr.yml @@ -4,6 +4,9 @@ on: push: tags: - v** + pull_request: + branches: + - master jobs: diff --git a/README.md b/README.md index b875491..9015f12 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,25 @@ # goslmailer -> **Info** +> **News & Info** +> +> v2.4.0 +> +> * discord connector +> * new connectors initialization code +> > Now also works with SLURM < 21.08 > > For templating differences between slurm>21.08 and slurm<21.08 see [templating guide](./templates/README.md) +> ## Drop-in notification delivery solution for slurm that can do: * message delivery to: + * [**discord**](https://discord.com/) * [**matrix**](https://matrix.org/) * [**telegram**](https://telegram.org/) * [**msteams**](https://teams.com) - * **e-mail** + * [**e-mail**](https://en.wikipedia.org/wiki/Email) * gathering of job **statistics** * generating **hints** for users on how to tune their job scripts (see examples below) * **templateable** messages ([readme](./templates/README.md)) @@ -43,6 +51,7 @@ To support future additional receiver schemes, a [connector package](connectors/ ## Currently available connectors: +* [**discord**](#discord-connector) bot --mail-user=`discord`:channelId * [**matrix**](#matrix-connector) bot --mail-user=`matrix:`roomId * [**telegram**](#telegram-connector) bot --mail-user=`telegram:`chatId * [**mailto**](#mailto-connector) --mail-user=`mailto:`email-addr @@ -56,7 +65,11 @@ See each connector details below... ## Building and installing -### Build +### Option 1. Download latest precompiled binaries [here](https://github.com/CLIP-HPC/goslmailer/releases/latest) + +Unpack, follow [instructions](#install) + +### Option 2. Build #### Quick version, without end to end testing @@ -114,6 +127,13 @@ make * config file has the same format as [goslmailer](cmd/goslmailer/goslmailer.conf.annotated_example), so you can use the same one (other connectors configs are not needed) * start the service (with -c switch pointing to config file) +#### discoslurmbot + +* place binary in a path to your liking +* place [discoslurmbot.conf](./cmd/discoslurmbot/discoslurmbot.conf) in a path to your liking + * config file has the same format as [goslmailer](cmd/goslmailer/goslmailer.conf.annotated_example), so you can use the same one (other connectors configs are not needed) +* start the service (with -c switch pointing to config file) + --- @@ -150,6 +170,14 @@ On startup, gobler reads its config file and spins-up a `connector monitor` for ## Connectors +| connector | spooling/throttling capable (gobler) | +|-----------|---------------------------| +| discord | yes | +| matrix | no | +| telegram | yes | +| msteams | yes | +| mailto | no | + ### default connector Specifies which receiver scheme is the default one, in case when user didn't specify `--mail-user` and slurm sent a bare username. @@ -185,6 +213,33 @@ See [annotated configuration example](cmd/goslmailer/goslmailer.conf.annotated_e --- +### discord connector + +Prerequisites for the discord connector: + +1. a discord bot must be created and +2. the bot daemon service **discoslurmbot** must be running ([example config file](./cmd/discoslurmbot/discoslurmbot.conf)). +3. once the bot is running, it will wake up on the configured `triggerString` and send the user a private message with slurm job submission instructions + + +#### Discord Bot setup + +1. User settings -> Advanced -> Developer mode ON +2. [Discord developer portal](https://discord.com/developers/applications) -> New Application -> Fill out BotName +3. Once the application is saved, select *Bot* from left menu -> Add Bot -> message: "A wild bot has appeared!" +4. Left menu: OAuth2 -> Copy Client ID +5. Modify this url with the Client ID from 4. and open in browser: `https://discord.com/api/oauth2/authorize?client_id=&permissions=8&scope=bot` +6. "An external application BotName wants to access your Discord Account" message -> Select server -> Continue +7. Grant Administrator permissions -> yes/no/maybe ? -> Authorize +8. [Discord developer portal](https://discord.com/developers/applications) -> Select BotName -> Bot menu -> Reset Token -> Copy and Save, to be used in discoslurmbot.conf + +Or follow this [tutorial](https://discordpy.readthedocs.io/en/stable/discord.html) + +![Discord card](./images/discord.png) + +* discord bot and packaged developed using [discordgo](https://github.com/bwmarrin/discordgo) and with the help of _Discord Gophers_ +--- + ### telegram connector Sends **1on1** or **group chat** messages about jobs via [telegram messenger app](https://telegram.org/) @@ -287,10 +342,6 @@ See [annotated configuration example](cmd/goslmailer/goslmailer.conf.annotated_e --- -## ToDo - ---- - ## Gotchas ### msteams diff --git a/VERSION b/VERSION index b1d18bc..8721bbc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.3.0 +v2.4.0 diff --git a/cmd/discoslurmbot/LICENSE b/cmd/discoslurmbot/LICENSE new file mode 100644 index 0000000..8d062ea --- /dev/null +++ b/cmd/discoslurmbot/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2015, Bruce Marriner +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of discordgo nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/cmd/discoslurmbot/discoslurmbot.conf b/cmd/discoslurmbot/discoslurmbot.conf new file mode 100644 index 0000000..1304b58 --- /dev/null +++ b/cmd/discoslurmbot/discoslurmbot.conf @@ -0,0 +1,11 @@ +{ # remember to remove comments from this json example ;) + "logfile": "", # if empty -> stderr, else log to specified file + "connectors": { + "discord": { + "name": "DiscoSlurmBot", # name that is used in the bot welcome message + "triggerString": "showmeslurm", # string (in channel or DM) that triggers the bot to respond with an instructional DM to the user + "token": "PasteBotTokenHere", # place to put the bot token + "messageTemplate": "/path/to/template.md" # template file to use + } + } +} diff --git a/cmd/discoslurmbot/discoslurmbot.go b/cmd/discoslurmbot/discoslurmbot.go new file mode 100644 index 0000000..5acf2dd --- /dev/null +++ b/cmd/discoslurmbot/discoslurmbot.go @@ -0,0 +1,154 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/CLIP-HPC/goslmailer/internal/cmdline" + "github.com/CLIP-HPC/goslmailer/internal/config" + "github.com/CLIP-HPC/goslmailer/internal/logger" + "github.com/CLIP-HPC/goslmailer/internal/version" + "github.com/bwmarrin/discordgo" +) + +const app = "discoslurmbot" + +type botConfig struct { + config.ConfigContainer + l *log.Logger +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the authenticated bot has access to. +// +// It is called whenever a message is created but only when it's sent through a +// server as we did not request IntentsDirectMessages. +func messageCreate(bc botConfig) func(*discordgo.Session, *discordgo.MessageCreate) { + return func(s *discordgo.Session, m *discordgo.MessageCreate) { + + fmt.Printf("session: %#v\n", s) + fmt.Printf("message: %#v\n", m) + fmt.Printf("message content: %#v\n", m.Content) + fmt.Printf("author: %#v\n", m.Author.ID) + fmt.Printf("user.id: %#v\n", s.State.User.ID) + + // Ignore all messages created by the bot itself + // This isn't required in this specific example but it's a good practice. + if m.Author.ID == s.State.User.ID { + return + } + // In this example, we only care about messages that are "ping". + if m.Content != bc.Connectors["discord"]["triggerString"] { + return + } + + // We create the private channel with the user who sent the message. + channel, err := s.UserChannelCreate(m.Author.ID) + if err != nil { + // If an error occurred, we failed to create the channel. + // + // Some common causes are: + // 1. We don't share a server with the user (not possible here). + // 2. We opened enough DM channels quickly enough for Discord to + // label us as abusing the endpoint, blocking us from opening + // new ones. + bc.l.Println("error creating channel:", err) + s.ChannelMessageSend( + m.ChannelID, + "Something went wrong while sending the DM!", + ) + return + } + // Then we send the message through the channel we created. + msg := fmt.Sprintf("Welcome,\nI am %s,\nplease use this switch in your job submission script in addition to '--mail-type' and i'll get back to you:\n '--mail-user=discord:%s'", bc.Connectors["discord"]["botname"], channel.ID) + _, err = s.ChannelMessageSend(channel.ID, msg) + if err != nil { + // If an error occurred, we failed to send the message. + // + // It may occur either when we do not share a server with the + // user (highly unlikely as we just received a message) or + // the user disabled DM in their settings (more likely). + bc.l.Println("error sending DM message:", err) + s.ChannelMessageSend( + m.ChannelID, + "Failed to send you a DM. "+ + "Did you disable DM in your privacy settings?", + ) + } + } +} + +func main() { + + // parse command line params + cmd, err := cmdline.NewCmdArgs(app) + if err != nil { + log.Fatalf("ERROR: parse command line failed with: %q\n", err) + } + + if *(cmd.Version) { + l := log.New(os.Stderr, app+":", log.Lshortfile|log.Ldate|log.Lmicroseconds) + version.DumpVersion(l) + os.Exit(0) + } + + // read config file + cfg := config.NewConfigContainer() + err = cfg.GetConfig(*(cmd.CfgFile)) + if err != nil { + log.Fatalf("ERROR: getConfig() failed: %s\n", err) + } + + // setup logger + l, err := logger.SetupLogger(cfg.Logfile, "gobler") + if err != nil { + log.Fatalf("setuplogger(%s) failed with: %q\n", cfg.Logfile, err) + } + + l.Println("===================== discoslurmbot start ======================================") + + version.DumpVersion(l) + + if _, ok := cfg.Connectors["discord"]["token"]; !ok { + l.Fatalf("MAIN: fetching config[connectors][discord][token] failed: %s\n", err) + } + + // Create a new Discord session using the provided bot token. + dg, err := discordgo.New("Bot " + cfg.Connectors["discord"]["token"]) + if err != nil { + l.Println("error creating Discord session,", err) + return + } + + // Register the messageCreate func as a callback for MessageCreate events. + bc := botConfig{ + *cfg, + l, + } + dg.AddHandler(messageCreate(bc)) + + // In this example, we only care about receiving message events. + // pja: and DMs + dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentDirectMessages + + // Open a websocket connection to Discord and begin listening. + err = dg.Open() + if err != nil { + l.Println("error opening connection,", err) + return + } + + // Wait here until CTRL-C or other term signal is received. + l.Println("Bot is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + // Cleanly close down the Discord session. + dg.Close() + + l.Println("===================== discoslurmbot end ========================================") +} diff --git a/cmd/gobler/gobler.go b/cmd/gobler/gobler.go index e88d01f..8d13b64 100644 --- a/cmd/gobler/gobler.go +++ b/cmd/gobler/gobler.go @@ -5,6 +5,11 @@ import ( "os" "sync" + _ "github.com/CLIP-HPC/goslmailer/connectors/discord" + _ "github.com/CLIP-HPC/goslmailer/connectors/mailto" + _ "github.com/CLIP-HPC/goslmailer/connectors/matrix" + _ "github.com/CLIP-HPC/goslmailer/connectors/msteams" + _ "github.com/CLIP-HPC/goslmailer/connectors/telegram" "github.com/CLIP-HPC/goslmailer/internal/cmdline" "github.com/CLIP-HPC/goslmailer/internal/config" "github.com/CLIP-HPC/goslmailer/internal/connectors" @@ -20,8 +25,7 @@ type MsgList []message.MessagePack func main() { var ( - conns = make(connectors.Connectors) - wg sync.WaitGroup + wg sync.WaitGroup ) // parse command line params @@ -30,7 +34,7 @@ func main() { log.Fatalf("ERROR: parse command line failed with: %q\n", err) } - if *(cmd.Version) == true { + if *(cmd.Version) { l := log.New(os.Stderr, "gobler:", log.Lshortfile|log.Ldate|log.Lmicroseconds) version.DumpVersion(l) os.Exit(0) @@ -56,7 +60,7 @@ func main() { cfg.DumpConfig(l) // populate map with configured referenced connectors - err = conns.PopulateConnectors(cfg, l) + err = connectors.ConMap.PopulateConnectors(cfg, l) if err != nil { l.Printf("MAIN: PopulateConnectors() failed with: %s\n", err) } @@ -74,7 +78,7 @@ func main() { continue } // func (cm *conMon) SpinUp(conns connectors.Connectors, wg sync.WaitGroup, l *log.Logger) error { - err = cm.SpinUp(conns, &wg, l) + err = cm.SpinUp(connectors.ConMap, &wg, l) if err != nil { l.Printf("MAIN: SpinUp(%s) failed with: %s\n", con, err) } diff --git a/cmd/goslmailer/goslmailer.conf.annotated_example b/cmd/goslmailer/goslmailer.conf.annotated_example index 5a30bc3..2e0fc37 100644 --- a/cmd/goslmailer/goslmailer.conf.annotated_example +++ b/cmd/goslmailer/goslmailer.conf.annotated_example @@ -33,6 +33,12 @@ "useLookup": "no", "format": "MarkdownV2" }, + "discord": { + "name": "DiscoSlurmBot", # name that is used in the bot welcome message + "triggerString": "showmeslurm", # string (in channel or DM) that triggers the bot to respond with an instructional DM to the user + "token": "PasteBotTokenHere", # place to put the bot token + "messageTemplate": "/path/to/template.md" # template file to use + } "matrix": { "username": "@myuser:matrix.org", "token": "syt_dGRpZG9ib3QXXXXXXXEyQMBEmvOVp_10Jm93", diff --git a/cmd/goslmailer/goslmailer.go b/cmd/goslmailer/goslmailer.go index 822c24c..6b9882a 100644 --- a/cmd/goslmailer/goslmailer.go +++ b/cmd/goslmailer/goslmailer.go @@ -6,6 +6,11 @@ import ( "log" "os" + _ "github.com/CLIP-HPC/goslmailer/connectors/discord" + _ "github.com/CLIP-HPC/goslmailer/connectors/mailto" + _ "github.com/CLIP-HPC/goslmailer/connectors/matrix" + _ "github.com/CLIP-HPC/goslmailer/connectors/msteams" + _ "github.com/CLIP-HPC/goslmailer/connectors/telegram" "github.com/CLIP-HPC/goslmailer/internal/config" "github.com/CLIP-HPC/goslmailer/internal/connectors" "github.com/CLIP-HPC/goslmailer/internal/message" @@ -20,7 +25,6 @@ func main() { var ( ic invocationContext job slurmjob.JobContext - conns = make(connectors.Connectors) logFile io.Writer ) @@ -77,7 +81,8 @@ func main() { job.GenerateHints(cfg.QosMap) // populate map with configured referenced connectors - conns.PopulateConnectors(cfg, log) + //conns.PopulateConnectors(cfg, log) + connectors.ConMap.PopulateConnectors(cfg, log) // Iterate over 'Receivers' map and for each call the connector.SendMessage() (if the receiver scheme is configured in conf file AND has an object in connectors map) if ic.Receivers == nil { @@ -89,7 +94,7 @@ func main() { if err != nil { log.Printf("ERROR in message.NewMsgPack(%s): %q\n", v.scheme, err) } - con, ok := conns[v.scheme] + con, ok := connectors.ConMap[v.scheme] if !ok { log.Printf("%s connector is not initialized for target %s. Ignoring.\n", v.scheme, v.target) } else { diff --git a/cmd/matrixslurmbot/matrixslurmbot.go b/cmd/matrixslurmbot/matrixslurmbot.go index 79c8bd3..e6b2cf9 100644 --- a/cmd/matrixslurmbot/matrixslurmbot.go +++ b/cmd/matrixslurmbot/matrixslurmbot.go @@ -14,6 +14,8 @@ import ( "maunium.net/go/mautrix/id" ) +const app = "matrixslurmbot" + func leaveAndForgetRoom(c *mautrix.Client, rid id.RoomID, l *log.Logger) error { l.Printf("Room - leaving: %s\n", rid) _, err := c.LeaveRoom(rid) @@ -88,13 +90,13 @@ func main() { ) // parse command line params - cmd, err := cmdline.NewCmdArgs("matrixslurmbot") + cmd, err := cmdline.NewCmdArgs(app) if err != nil { log.Fatalf("ERROR: parse command line failed with: %q\n", err) } if *(cmd.Version) { - l = log.New(os.Stderr, "matrixslurmbot:", log.Lshortfile|log.Ldate|log.Lmicroseconds) + l = log.New(os.Stderr, app+":", log.Lshortfile|log.Ldate|log.Lmicroseconds) version.DumpVersion(l) os.Exit(0) } @@ -152,20 +154,20 @@ func main() { // do we need to keep any state at all? syncer.OnEvent(client.Store.(*mautrix.InMemoryStore).UpdateState) - /* - //START code for responding to user messages - //disabled for now since it only works on unencrypted channels - - syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, event *event.Event) { - //TODO: implement this for encrypted channels, only works for - //unencrypted right now - body := event.Content.Raw["body"].(string) - if strings.HasPrefix(body, "!bot"){ - client.SendText(event.RoomID, fmt.Sprintf("Sorry, I'm still a bit dumb. Use this switch in your job submission script and i'll get back to you:\n--mail-user=matrix:%s\n", string(event.RoomID))) - } - }) - //END code for responding to user messages - */ + /* + //START code for responding to user messages + //disabled for now since it only works on unencrypted channels + + syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, event *event.Event) { + //TODO: implement this for encrypted channels, only works for + //unencrypted right now + body := event.Content.Raw["body"].(string) + if strings.HasPrefix(body, "!bot"){ + client.SendText(event.RoomID, fmt.Sprintf("Sorry, I'm still a bit dumb. Use this switch in your job submission script and i'll get back to you:\n--mail-user=matrix:%s\n", string(event.RoomID))) + } + }) + //END code for responding to user messages + */ syncer.OnEventType(event.StateMember, func(source mautrix.EventSource, event *event.Event) { l.Printf("--------------------------------------------------------------------------------\n") diff --git a/cmd/tgslurmbot/tgslurmbot.go b/cmd/tgslurmbot/tgslurmbot.go index 457f168..6595cca 100644 --- a/cmd/tgslurmbot/tgslurmbot.go +++ b/cmd/tgslurmbot/tgslurmbot.go @@ -13,6 +13,8 @@ import ( tele "gopkg.in/telebot.v3" ) +const app = "tgslurmbot" + func main() { var ( @@ -21,13 +23,13 @@ func main() { ) // parse command line params - cmd, err := cmdline.NewCmdArgs("tgslurmbot") + cmd, err := cmdline.NewCmdArgs(app) if err != nil { log.Fatalf("ERROR: parse command line failed with: %q\n", err) } if *(cmd.Version) == true { - l = log.New(os.Stderr, "tgslurmbot:", log.Lshortfile|log.Ldate|log.Lmicroseconds) + l = log.New(os.Stderr, app+":", log.Lshortfile|log.Ldate|log.Lmicroseconds) version.DumpVersion(l) os.Exit(0) } diff --git a/connectors/connectorX/README.md b/connectors/connectorX/README.md index 29419ff..fbab8d8 100644 --- a/connectors/connectorX/README.md +++ b/connectors/connectorX/README.md @@ -14,7 +14,8 @@ Files you'll need to get started: ## Exercise for the reader: -To make this connector work, add the missing code block to the [connectors package](../../internal/connectors/connectors.go). +To make this connector work, add the missing blank import to [goslmailer main package](../../cmd/goslmailer/goslmailer.go) to trigger init(). + Recompile and try it out. To verify it works: diff --git a/connectors/connectorX/connectorX.go b/connectors/connectorX/connectorX.go index 0163414..8fc64a6 100644 --- a/connectors/connectorX/connectorX.go +++ b/connectors/connectorX/connectorX.go @@ -10,36 +10,41 @@ import ( "strconv" "time" + "github.com/CLIP-HPC/goslmailer/internal/connectors" "github.com/CLIP-HPC/goslmailer/internal/lookup" "github.com/CLIP-HPC/goslmailer/internal/message" "github.com/CLIP-HPC/goslmailer/internal/renderer" "github.com/CLIP-HPC/goslmailer/internal/spool" ) -// NewConnector instantiates a connectorX.Connector structure with values read from config file. -// Mandatory. -// Call has to be added here: ../../internal/connectors/connectors.go:25 -func NewConnector(conf map[string]string) (*Connector, error) { - // declare the Connector structure and assign the values from config file, whatever the new connector needs - c := Connector{ - name: conf["name"], - addr: conf["addr"], - port: conf["port"], - templateFile: conf["templateFile"], - renderToFile: conf["renderToFile"], - spoolDir: conf["spoolDir"], - useLookup: conf["useLookup"], - } +// init registers the new connector with the connectors package +func init() { + connectors.Register(connectorName, connConnectorX) +} + +// ConfigConnector fills out the package Connector structure with values from config file. +// Recommended to also do sanity checking of config values here. Mandatory. +// Makes connectorX.Connector type satisfy the connectors.Connector interface. +func (c *Connector) ConfigConnector(conf map[string]string) error { + // Fill out the Connector structure with values from config file + c.name = conf["name"] + c.addr = conf["addr"] + c.port = conf["port"] + c.templateFile = conf["templateFile"] + c.renderToFile = conf["renderToFile"] + c.spoolDir = conf["spoolDir"] + c.useLookup = conf["useLookup"] + // Here you can do sanity checking and defaulting if needed. // e.g. // if renderToFile=="no" or "spool" then spoolDir must not be empty switch c.renderToFile { case "no", "spool": if c.spoolDir == "" { - return nil, errors.New("spoolDir must be defined, aborting") + return errors.New("spoolDir must be defined, aborting") } } - return &c, nil + return nil } // SendMessage is the main method of the connector code, it usually does something like: diff --git a/connectors/connectorX/connector_data.go b/connectors/connectorX/connector_data.go index b36effd..b9ca831 100644 --- a/connectors/connectorX/connector_data.go +++ b/connectors/connectorX/connector_data.go @@ -10,6 +10,9 @@ package connectorX import "log" +// Name of the connector, used in the init() function to Register() it to connectors package. +const connectorName = "discord" + // Connector structure contains configuration data read in from config file with connectorX.NewConnector(). // Populate this structure with the configuration variables a new connector needs type Connector struct { @@ -35,3 +38,6 @@ func (c *Connector) dumpConnector(l *log.Logger) { l.Println("................................................................................") } + +// Variable holding the connector configuration, mandatory +var connConnectorX *Connector = new(Connector) diff --git a/connectors/discord/connector_data.go b/connectors/discord/connector_data.go new file mode 100644 index 0000000..2ddaa80 --- /dev/null +++ b/connectors/discord/connector_data.go @@ -0,0 +1,31 @@ +package discord + +import "log" + +const connectorName = "discord" + +type Connector struct { + name string + triggerString string + token string + renderToFile string + spoolDir string + messageTemplate string + useLookup string + format string +} + +func (c *Connector) dumpConnector(l *log.Logger) { + l.Printf("discord.dumpConnector: name: %q\n", c.name) + l.Printf("discord.dumpConnector: triggerstring: %q\n", c.triggerString) + l.Printf("discord.dumpConnector: token: PRESENT\n") + l.Printf("discord.dumpConnector: renderToFile: %q\n", c.renderToFile) + l.Printf("discord.dumpConnector: spoolDir: %q\n", c.spoolDir) + l.Printf("discord.dumpConnector: messageTemplate: %q\n", c.messageTemplate) + l.Printf("discord.dumpConnector: useLookup: %q\n", c.useLookup) + l.Printf("discord.dumpConnector: format: %q\n", c.format) + l.Println("................................................................................") + +} + +var connDiscord *Connector = new(Connector) diff --git a/connectors/discord/discord.go b/connectors/discord/discord.go new file mode 100644 index 0000000..306488d --- /dev/null +++ b/connectors/discord/discord.go @@ -0,0 +1,148 @@ +package discord + +import ( + "bytes" + "errors" + "io" + "log" + "os" + "strconv" + "time" + + "github.com/CLIP-HPC/goslmailer/internal/connectors" + "github.com/CLIP-HPC/goslmailer/internal/lookup" + "github.com/CLIP-HPC/goslmailer/internal/message" + "github.com/CLIP-HPC/goslmailer/internal/renderer" + "github.com/CLIP-HPC/goslmailer/internal/spool" + "github.com/bwmarrin/discordgo" +) + +func init() { + connectors.Register(connectorName, connDiscord) +} + +func (c *Connector) ConfigConnector(conf map[string]string) error { + + // here we need some test if the connectors "minimal" configuration is satisfied, e.g. must have url at minimum + c.name = conf["name"] + c.triggerString = conf["triggerString"] + c.token = conf["token"] + c.renderToFile = conf["renderToFile"] + c.spoolDir = conf["spoolDir"] + c.messageTemplate = conf["messageTemplate"] + c.useLookup = conf["useLookup"] + c.format = conf["format"] + + switch { + // token must be present + case c.token == "": + return errors.New("discord bot token must be defined, aborting") + // if renderToFile=="no" or "spool" then spoolDir must not be empty + case c.renderToFile == "no" || c.renderToFile == "spool": + if c.spoolDir == "" { + return errors.New("discord spoolDir must be defined, aborting") + } + + } + + return nil +} + +func (c *Connector) SendMessage(mp *message.MessagePack, useSpool bool, l *log.Logger) error { + + var ( + e error = nil + outFile string + dts bool = false // DumpToSpool + buffer bytes.Buffer + ) + + l.Println("................... sendTodiscord START ........................................") + + // debug purposes + c.dumpConnector(l) + + // spin up new bot + // Create a new Discord session using the provided bot token. + dg, err := discordgo.New("Bot " + c.token) + if err != nil { + l.Println("error creating Discord session,", err) + return err + } + + // lookup the end-system userid from the one sent by slurm (if lookup is set in "useLookup" config param) + enduser, err := lookup.ExtLookupUser(mp.TargetUser, c.useLookup, l) + if err != nil { + l.Printf("Lookup failed for %s with %s\n", mp.TargetUser, err) + return err + } + l.Printf("Looked up with %q %s -> %s\n", c.useLookup, mp.TargetUser, enduser) + + l.Printf("Sending to targetUserID: %s\n", enduser) + + // don't render template when using spool + if c.renderToFile != "spool" { + // buffer to place rendered json in + buffer = bytes.Buffer{} + //err := c.discordRenderTemplate(mp.JobContext, enduser, &buffer) + err := renderer.RenderTemplate(c.messageTemplate, c.format, mp.JobContext, enduser, &buffer) + if err != nil { + return err + } + } + + // this can be: "yes", "spool", anythingelse + switch c.renderToFile { + case "yes": + // render template to a file in working directory - debug purposes + // prepare outfile name + t := strconv.FormatInt(time.Now().UnixNano(), 10) + l.Printf("Time: %s\n", t) + outFile = "rendered-" + mp.JobContext.SLURM_JOB_ID + "-" + enduser + "-" + t + ".msg" + res, err := io.ReadAll(&buffer) + if err != nil { + return err + } + err = os.WriteFile(outFile, res, 0644) + if err != nil { + return err + } + l.Printf("Send successful to file: %s\n", outFile) + case "spool": + // deposit GOB to spoolDir if allowed + if useSpool { + err := spool.DepositToSpool(c.spoolDir, mp) + if err != nil { + l.Printf("DepositToSpool Failed!\n") + return err + } + } + default: + // Then we send the message through the channel we created. + //_, err = dg.ChannelMessageSend(enduser, "A successfull message at "+time.Now().String()) + _, err = dg.ChannelMessageSend(enduser, buffer.String()) + if err != nil { + l.Printf("error sending DM message: %s\n", err) + dts = true + } else { + l.Printf("bot.Send() successful\n") + dts = false + } + + dg.Close() + } + + // save mp to spool if we're allowed (not allowed when called from gobler, to prevent gobs multiplying) + if dts && useSpool { + l.Printf("Backing off to spool.\n") + err := spool.DepositToSpool(c.spoolDir, mp) + if err != nil { + l.Printf("DepositToSpool Failed!\n") + return err + } + } + + l.Println("................... sendTodiscord END ..........................................") + + return e +} diff --git a/connectors/mailto/connector_data.go b/connectors/mailto/connector_data.go index 013c3d9..474c71e 100644 --- a/connectors/mailto/connector_data.go +++ b/connectors/mailto/connector_data.go @@ -1,5 +1,7 @@ package mailto +const connectorName = "mailto" + type Connector struct { name string mailCmd string @@ -9,3 +11,5 @@ type Connector struct { allowList string blockList string } + +var connMailto *Connector = new(Connector) diff --git a/connectors/mailto/mailto.go b/connectors/mailto/mailto.go index a231291..1e25b82 100644 --- a/connectors/mailto/mailto.go +++ b/connectors/mailto/mailto.go @@ -8,22 +8,28 @@ import ( "regexp" "text/template" + "github.com/CLIP-HPC/goslmailer/internal/connectors" "github.com/CLIP-HPC/goslmailer/internal/message" "github.com/CLIP-HPC/goslmailer/internal/renderer" ) -func NewConnector(conf map[string]string) (*Connector, error) { +func init() { + connectors.Register(connectorName, connMailto) +} + +func (c *Connector) ConfigConnector(conf map[string]string) error { + c.name = conf["name"] + c.mailCmd = conf["mailCmd"] + c.mailCmdParams = conf["mailCmdParams"] + c.mailTemplate = conf["mailTemplate"] + c.mailFormat = conf["mailFormat"] + c.allowList = conf["allowList"] + c.blockList = conf["blockList"] + // here we need some test if the connectors "minimal" configuration is satisfied, e.g. must have url at minimum - c := Connector{ - name: conf["name"], - mailCmd: conf["mailCmd"], - mailCmdParams: conf["mailCmdParams"], - mailTemplate: conf["mailTemplate"], - mailFormat: conf["mailFormat"], - allowList: conf["allowList"], - blockList: conf["blockList"], - } - return &c, nil + // + // if ok, return nil error + return nil } func (c *Connector) SendMessage(mp *message.MessagePack, useSpool bool, l *log.Logger) error { diff --git a/connectors/matrix/connector_data.go b/connectors/matrix/connector_data.go index 1dcf72f..89275e5 100644 --- a/connectors/matrix/connector_data.go +++ b/connectors/matrix/connector_data.go @@ -1,8 +1,12 @@ package matrix +const connectorName = "matrix" + type Connector struct { username string token string homeserver string template string } + +var connMatrix *Connector = new(Connector) diff --git a/connectors/matrix/matrix.go b/connectors/matrix/matrix.go index d6c1d00..261b6c0 100644 --- a/connectors/matrix/matrix.go +++ b/connectors/matrix/matrix.go @@ -4,6 +4,7 @@ import ( "bytes" "log" + "github.com/CLIP-HPC/goslmailer/internal/connectors" "github.com/CLIP-HPC/goslmailer/internal/message" "github.com/CLIP-HPC/goslmailer/internal/renderer" @@ -13,14 +14,21 @@ import ( "maunium.net/go/mautrix/id" ) -func NewConnector(conf map[string]string) (*Connector, error) { - c := Connector{ - username: conf["username"], - token: conf["token"], - homeserver: conf["homeserver"], - template: conf["template"], - } - return &c, nil +func init() { + connectors.Register(connectorName, connMatrix) +} + +func (c *Connector) ConfigConnector(conf map[string]string) error { + + c.username = conf["username"] + c.token = conf["token"] + c.homeserver = conf["homeserver"] + c.template = conf["template"] + + // here we need some test if the connectors "minimal" configuration is satisfied, e.g. must have url at minimum + // + // if ok, return nil error + return nil } func (c *Connector) SendMessage(mp *message.MessagePack, useSpool bool, l *log.Logger) error { diff --git a/connectors/msteams/connector_data.go b/connectors/msteams/connector_data.go index e848260..daa2d13 100644 --- a/connectors/msteams/connector_data.go +++ b/connectors/msteams/connector_data.go @@ -2,6 +2,8 @@ package msteams import "log" +const connectorName = "msteams" + type Connector struct { name string url string @@ -22,3 +24,5 @@ func (c *Connector) dumpConnector(l *log.Logger) { l.Println("................................................................................") } + +var connMsteams *Connector = new(Connector) diff --git a/connectors/msteams/msteams.go b/connectors/msteams/msteams.go index bed967f..83bdc75 100644 --- a/connectors/msteams/msteams.go +++ b/connectors/msteams/msteams.go @@ -10,30 +10,34 @@ import ( "strconv" "time" + "github.com/CLIP-HPC/goslmailer/internal/connectors" "github.com/CLIP-HPC/goslmailer/internal/lookup" "github.com/CLIP-HPC/goslmailer/internal/message" "github.com/CLIP-HPC/goslmailer/internal/renderer" "github.com/CLIP-HPC/goslmailer/internal/spool" ) -func NewConnector(conf map[string]string) (*Connector, error) { - // here we need some test if the connectors "minimal" configuration is satisfied, e.g. must have url at minimum - c := Connector{ - name: conf["name"], - url: conf["url"], - renderToFile: conf["renderToFile"], - spoolDir: conf["spoolDir"], - adaptiveCardTemplate: conf["adaptiveCardTemplate"], - useLookup: conf["useLookup"], - } +func init() { + connectors.Register(connectorName, connMsteams) +} + +func (c *Connector) ConfigConnector(conf map[string]string) error { + + c.name = conf["name"] + c.url = conf["url"] + c.renderToFile = conf["renderToFile"] + c.spoolDir = conf["spoolDir"] + c.adaptiveCardTemplate = conf["adaptiveCardTemplate"] + c.useLookup = conf["useLookup"] + // if renderToFile=="no" or "spool" then spoolDir must not be empty switch c.renderToFile { case "no", "spool": if c.spoolDir == "" { - return nil, errors.New("spoolDir must be defined, aborting") + return errors.New("spoolDir must be defined, aborting") } } - return &c, nil + return nil } func (c *Connector) SendMessage(mp *message.MessagePack, useSpool bool, l *log.Logger) error { diff --git a/connectors/telegram/connector_data.go b/connectors/telegram/connector_data.go index 0b9748a..7dc27c0 100644 --- a/connectors/telegram/connector_data.go +++ b/connectors/telegram/connector_data.go @@ -2,6 +2,8 @@ package telegram import "log" +const connectorName = "telegram" + type Connector struct { name string url string @@ -25,3 +27,5 @@ func (c *Connector) dumpConnector(l *log.Logger) { l.Println("................................................................................") } + +var connTelegram *Connector = new(Connector) diff --git a/connectors/telegram/telegram.go b/connectors/telegram/telegram.go index 75c75d4..2a37d1e 100644 --- a/connectors/telegram/telegram.go +++ b/connectors/telegram/telegram.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "github.com/CLIP-HPC/goslmailer/internal/connectors" "github.com/CLIP-HPC/goslmailer/internal/lookup" "github.com/CLIP-HPC/goslmailer/internal/message" "github.com/CLIP-HPC/goslmailer/internal/renderer" @@ -16,26 +17,29 @@ import ( telebot "gopkg.in/telebot.v3" ) -func NewConnector(conf map[string]string) (*Connector, error) { - // here we need some test if the connectors "minimal" configuration is satisfied, e.g. must have url at minimum - c := Connector{ - name: conf["name"], - url: conf["url"], - token: conf["token"], - renderToFile: conf["renderToFile"], - spoolDir: conf["spoolDir"], - messageTemplate: conf["messageTemplate"], - useLookup: conf["useLookup"], - format: conf["format"], - } +func init() { + connectors.Register(connectorName, connTelegram) +} + +func (c *Connector) ConfigConnector(conf map[string]string) error { + + c.name = conf["name"] + c.url = conf["url"] + c.token = conf["token"] + c.renderToFile = conf["renderToFile"] + c.spoolDir = conf["spoolDir"] + c.messageTemplate = conf["messageTemplate"] + c.useLookup = conf["useLookup"] + c.format = conf["format"] + // if renderToFile=="no" or "spool" then spoolDir must not be empty switch c.renderToFile { case "no", "spool": if c.spoolDir == "" { - return nil, errors.New("telegram spoolDir must be defined, aborting") + return errors.New("telegram spoolDir must be defined, aborting") } } - return &c, nil + return nil } func (c *Connector) SendMessage(mp *message.MessagePack, useSpool bool, l *log.Logger) error { diff --git a/go.mod b/go.mod index 3e9616c..7d8e44b 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,17 @@ module github.com/CLIP-HPC/goslmailer go 1.17 -require github.com/dustin/go-humanize v1.0.0 - -require gopkg.in/telebot.v3 v3.0.0 +require ( + github.com/bwmarrin/discordgo v0.25.0 + github.com/dustin/go-humanize v1.0.0 + gopkg.in/telebot.v3 v3.0.0 + maunium.net/go/mautrix v0.11.0 +) require ( + github.com/gorilla/websocket v1.5.0 // indirect github.com/yuin/goldmark v1.4.12 // indirect golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect - maunium.net/go/mautrix v0.11.0 // indirect + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect ) diff --git a/go.sum b/go.sum index c32d017..7930701 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs= +github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -10,47 +12,72 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0= golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo= gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/mautrix v0.11.0 h1:B1FBHcvE4Mud+AC+zgNQQOw0JxSVrt40watCejhVA7w= maunium.net/go/mautrix v0.11.0/go.mod h1:K29EcHwsNg6r7fMfwvi0GHQ9o5wSjqB9+Q8RjCIQEjA= diff --git a/images/discord.png b/images/discord.png new file mode 100644 index 0000000..34d96e3 Binary files /dev/null and b/images/discord.png differ diff --git a/internal/connectors/connectors.go b/internal/connectors/connectors.go index 8489b59..eac7dd9 100644 --- a/internal/connectors/connectors.go +++ b/internal/connectors/connectors.go @@ -3,71 +3,53 @@ package connectors import ( "log" - "github.com/CLIP-HPC/goslmailer/connectors/mailto" - "github.com/CLIP-HPC/goslmailer/connectors/matrix" - "github.com/CLIP-HPC/goslmailer/connectors/msteams" - "github.com/CLIP-HPC/goslmailer/connectors/telegram" "github.com/CLIP-HPC/goslmailer/internal/config" "github.com/CLIP-HPC/goslmailer/internal/message" ) type Connector interface { - //SendMessage(mp *message.MessagePack, useSpool bool, l *log.Logger) error + ConfigConnector(conf map[string]string) error SendMessage(*message.MessagePack, bool, *log.Logger) error } type Connectors map[string]Connector +var ConMap Connectors = Connectors{} + +func Register(conName string, conStruct Connector) error { + + if _, ok := ConMap[conName]; !ok { + log.Printf("Initializing connector: %s\n", conName) + ConMap[conName] = conStruct + } else { + log.Printf("Connector %s already initialized.\n", conName) + } + + return nil +} + // Populate the map 'connectors' with connectors specified in config file and their instance from package. // Every newly developed connector must have a case block added here. func (c *Connectors) PopulateConnectors(conf *config.ConfigContainer, l *log.Logger) error { - // Iterate through map of connectors from config file. + for k, v := range conf.Connectors { - switch k { - case "mailto": - // For each recognized, call the connectorpkg.NewConnector() and... - // todo: make this a little bit less ugly... - con, err := mailto.NewConnector(v) - if err != nil { - l.Printf("Problem: %q with %s connector configuration. Ignoring.\n", err, k) - break - } - l.Printf("%s connector configured.\n", k) - // ...asign its return object value to the connectors map. - (*c)[k] = con - case "msteams": - // For each recognized, call the connectorpkg.NewConnector() and... - con, err := msteams.NewConnector(v) - if err != nil { - l.Printf("Problem: %q with %s connector configuration. Ignoring.\n", err, k) - break - } - l.Printf("%s connector configured.\n", k) - // ...asign its return object value to the connectors map. - (*c)[k] = con - case "telegram": - // For each recognized, call the connectorpkg.NewConnector() and... - con, err := telegram.NewConnector(v) - if err != nil { - l.Printf("Problem: %q with %s connector configuration. Ignoring.\n", err, k) - break - } - l.Printf("%s connector configured.\n", k) - // ...asign its return object value to the connectors map. - (*c)[k] = con - case "matrix": - // For each recognized, call the connectorpkg.NewConnector() and... - con, err := matrix.NewConnector(v) - if err != nil { - l.Printf("Problem: %q with %s connector configuration. Ignoring.\n", err, k) - break - } - l.Printf("%s connector configured.\n", k) - // ...asign its return object value to the connectors map. - (*c)[k] = con - default: - l.Printf("Unsupported connector found. Ignoring %#v : %#v\n", k, v) + // test if connector from config is registered in conMap + if _, ok := (*c)[k]; !ok { + l.Printf("ERROR: %q connector not initialized, skipping...\n", k) + continue + } + // l.Printf("Unsupported connector found. Ignoring %#v : %#v\n", k, v) + // if it is, try to configure it + l.Printf("CONFIGURING: %s with: %#v\n", k, v) + if err := (*c)[k].ConfigConnector(v); err != nil { + // config failed, log and remove from map + l.Printf("ERROR: %q with %s connector configuration. Ignoring.\n", err, k) + delete(*c, k) + } else { + // config successfull, log and do nothing. + l.Printf("SUCCESS: %s connector configured.\n", k) } } + return nil } diff --git a/internal/connectors/connectors_test.go b/internal/connectors/connectors_test.go index 40bc121..b3cb204 100644 --- a/internal/connectors/connectors_test.go +++ b/internal/connectors/connectors_test.go @@ -5,6 +5,11 @@ import ( "log" "testing" + _ "github.com/CLIP-HPC/goslmailer/connectors/discord" + _ "github.com/CLIP-HPC/goslmailer/connectors/mailto" + _ "github.com/CLIP-HPC/goslmailer/connectors/matrix" + _ "github.com/CLIP-HPC/goslmailer/connectors/msteams" + _ "github.com/CLIP-HPC/goslmailer/connectors/telegram" "github.com/CLIP-HPC/goslmailer/internal/config" "github.com/CLIP-HPC/goslmailer/internal/connectors" ) @@ -13,9 +18,6 @@ var connectorsExpected = []string{"msteams", "mailto"} var connectorsExpectedNot = []string{"textfile"} func TestPopulateConnectors(t *testing.T) { - var ( - conns = make(connectors.Connectors) - ) wr := bytes.Buffer{} l := log.New(&wr, "Testing: ", log.Llongfile) @@ -26,7 +28,7 @@ func TestPopulateConnectors(t *testing.T) { t.Fatalf("MAIN: getConfig(gobconfig) failed: %s", err) } - err = conns.PopulateConnectors(cfg, l) + err = connectors.ConMap.PopulateConnectors(cfg, l) if err != nil { t.Fatalf("conns.PopulateConnectors() FAILED with %s\n", err) } @@ -34,7 +36,7 @@ func TestPopulateConnectors(t *testing.T) { t.Run("connectorsExpected", func(t *testing.T) { for _, v := range connectorsExpected { t.Logf("Testing for connector %s", v) - if _, ok := conns[v]; !ok { + if _, ok := connectors.ConMap[v]; !ok { t.Fatalf("Connector %s not configured!", v) } else { t.Logf("FOUND... good!\n") @@ -44,7 +46,7 @@ func TestPopulateConnectors(t *testing.T) { t.Run("connectorsExpectedNot", func(t *testing.T) { for _, v := range connectorsExpectedNot { t.Logf("Testing for connector %s", v) - if _, ok := conns[v]; ok { + if _, ok := connectors.ConMap[v]; ok { t.Fatalf("Connector %s configured but must NOT be!", v) } else { t.Logf("NOT FOUND... good!\n") diff --git a/templates/README.md b/templates/README.md index f952ef1..7c0d529 100644 --- a/templates/README.md +++ b/templates/README.md @@ -17,7 +17,8 @@ Example: * `{{ .Job.SlurmEnvironment.SLURM_JOB_MAIL_TYPE }}` * `{{ .Job.JobStats.MaxRSS | humanBytes }}` -[Example telegram html template](./telegramTemplate.html) +* [Example telegram html template](./telegramTemplate.html) +* [More template examples](./templates/) Structures: