/* Copyright 2018 Martin Dosch Licensed under the "MIT License" */ package main import ( "encoding/json" "flag" "log" "os" "os/user" "strings" "time" "github.com/chilts/sid" "github.com/mattn/go-xmpp" ) // Variables defined globally as they are used by functions pingMUC // and processStanzas. var ( id string err error pingSent time.Time pingReceived bool ) func main() { var configPath, configFile string type configuration struct { ServerAddress string BotJid string Password string Muc string MucNick string MaxArticles int RefreshTime time.Duration NoExcerpt bool Quiet bool Contact string Filter []string Feeds []string } // Read path to config from command line option. configFilePtr := flag.String("config", "none", "path to configuration file") flag.Parse() if *configFilePtr != "none" { configFile = *configFilePtr } else { // Get systems user config path. osConfigDir := os.Getenv("$XDG_CONFIG_HOME") if osConfigDir != "" { // Create configPath if not yet existing. configPath = osConfigDir + "/.config/feed-to-muc/" if _, err := os.Stat(configPath); os.IsNotExist(err) { err = os.MkdirAll(configPath, 0700) if err != nil { log.Fatal("Error: Can't create config path: ", err) } } } else { // Get the current user. curUser, err := user.Current() if err != nil { log.Fatal("Error: Can't get current user: ", err) return } // Get home directory. home := curUser.HomeDir if home == "" { log.Fatal("Error: No home directory available.") return } // Create configPath if not yet existing. configPath = home + "/.config/feed-to-muc/" if _, err := os.Stat(configPath + "config.json"); os.IsNotExist(err) { err = os.MkdirAll(configPath, 0700) if err != nil { log.Fatal("Error: Can't create config path: ", err) } } } configFile = configPath + "config.json" } // Check that config file is existing. if _, err := os.Stat(configFile); os.IsNotExist(err) { log.Fatal("Error: Can't find config file: ", err) } // Read configuration file into variable config. file, _ := os.Open(configFile) defer file.Close() decoder := json.NewDecoder(file) config := configuration{} if err := decoder.Decode(&config); err != nil { log.Fatal("Error: Can't decode config: ", err) } if _, err := os.Stat(configFile); os.IsNotExist(err) { err = os.MkdirAll(configPath, 0700) if err != nil { log.Fatal("Error: Can't create config path: ", err) } } var client *xmpp.Client options := xmpp.Options{ Host: config.ServerAddress, User: config.BotJid, Password: config.Password, NoTLS: true, StartTLS: true, Debug: false, } // Connect to server. client, err = options.NewClient() if err != nil { log.Fatal("Error: Can't connect to xmpp server: ", err) } // Join the MUC _, err := client.JoinMUCNoHistory(config.Muc, config.MucNick) if err != nil { log.Fatal("Error: Can't join MUC: ", err) } // Starting goroutine to ping the server every 30 seconds. go pingServer(client, config.ServerAddress, config.BotJid) // Starting goroutine to ping the MUC every 30 seconds. go pingMUC(client, config.BotJid, config.Muc, config.MucNick) // Starting goroutine to process received stanzas. go processStanzas(client, config.Muc, config.MucNick, config.Feeds, config.Quiet, config.Contact) // Set RefreshTime to 30 seconds if not defined. if config.RefreshTime == 0 { config.RefreshTime = 30 } for { // Check all configured feeds for new articles and send // new articles to configured MUC. for i := 0; i < len(config.Feeds); i++ { output, err := getArticles(config.Feeds[i], config.MaxArticles, config.NoExcerpt) if err != nil { // Exit if an error occurs checking the feeds. log.Fatal("Error: Can't check feeds for new articles: ", err) } if output != "" { _, err = client.Send(xmpp.Chat{Remote: config.Muc, Type: "groupchat", Text: output}) if err != nil { // ToDo: Save message for resend. // Exit if message can not be sent. log.Fatal("Error: Can't send message to MUC: ", err) } } } // Wait 30 seconds before checking feeds again. time.Sleep(config.RefreshTime * time.Second) } } // Send a ping every 30 seconds after last successful ping to check if the MUC is still available. func pingMUC(client *xmpp.Client, botJid string, muc string, mucNick string) { for { time.Sleep(30 * time.Second) // Send ping to own MUC participant to check we are still joined. id, err = client.RawInformation(botJid, muc+"/"+mucNick, sid.Id(), "get", "") if err != nil { log.Fatal("Error: Can't send MUC self ping: ", err) } pingSent = time.Now() pingReceived = false // Check for result IQ as long as there was no reply yet. for (time.Since(pingSent).Seconds() < 10.0) && (!pingReceived) { time.Sleep(1 * time.Second) if pingReceived { break } } // Quit if no ping reply was received. if !pingReceived { log.Fatal("MUC not available.") } } } // Send a ping to the server every 30 seconds to check if the connection is still alive. func pingServer(client *xmpp.Client, server string, botJid string) { for { time.Sleep(30 * time.Second) // Send ping to server to check if connection still exists. err := client.PingC2S(botJid, strings.Split(server, ":")[0]) if err != nil { log.Fatal("Error: Client2Server ping failed:", err) } } } func processStanzas(client *xmpp.Client, muc string, mucNick string, feeds []string, quiet bool, contact string) { for { // Receive stanzas. ToDo: Receive stanzas continiously without 30s delay. stanza, err := client.Recv() if err != nil { log.Fatal("Error: Failed receiving Stanzas:", err) } // Check stanzas, maybe we want to reply. switch v := stanza.(type) { // Reply to requests for source and feeds. case xmpp.Chat: var command string // Check for room mention of the bots nick if the the message type is groupchat. if v.Type == "groupchat" { // Leave if option quiet is set. //if quiet { // break //} // Get first word of the message and transform it to lower case. mention := strings.ToLower(strings.Split(v.Text, " ")[0]) // If it is not the bots nick remove one trailing character as // a lot of clients append `:` or `,` to mentions. if mention != strings.ToLower(mucNick) { mentionLength := len(mention) // Leave if mentionLength is <= 0 if mentionLength <= 0 { break } mention = mention[:mentionLength-1] // Leave if the message is not addressed to the bot. if mention != strings.ToLower(mucNick) { break } } // As the first word is the mention of the bots nickname, the command is // the second word in a groupchat message. // command = strings.ToLower(strings.Split(v.Text, " ")[1]) // If the message type is chat (e.g. private message), the command is the // first word. reply := "Bitte nicht mit dem Bot sprechen.\nhttps://www.kuketz-blog.de/chat/#bots" _, err = client.Send(xmpp.Chat{Remote: muc, Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] + ": " + reply}) if err != nil { log.Fatal("Error: Failed sending message to MUC:", err) } break } else if v.Type == "chat" { command = strings.ToLower(strings.Split(v.Text, " ")[0]) } else { break } // Check for the command. switch command { // Reply with a short summary of available commands for `help`. case "help": reply := "The following commands are available:\n" + "\"contact\": Show contact for this bot.\n" + "\"feeds\": List feeds I'm following.\n" + "\"ping\": Sends back a pong.\n" + "\"source\": Show source code URL." if v.Type == "groupchat" { _, err = client.Send(xmpp.Chat{Remote: muc, Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] + ": " + reply}) if err != nil { log.Fatal("Error: Failed sending message to MUC:", err) } } else if v.Type == "chat" { _, err = client.Send(xmpp.Chat{Remote: v.Remote, Type: "chat", Text: reply}) if err != nil { log.Fatal("Error: Failed sending message to ", v.Remote, ": ", err) } } // Reply with repo address for `source`. case "source": reply := "My source can be found at " + "https://salsa.debian.org/mdosch-guest/feed-to-muc" if v.Type == "groupchat" { _, err = client.Send(xmpp.Chat{Remote: muc, Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] + ": " + reply}) if err != nil { log.Fatal("Error: Failed sending message to MUC:", err) } } else if v.Type == "chat" { _, err = client.Send(xmpp.Chat{Remote: v.Remote, Type: "chat", Text: reply}) if err != nil { log.Fatal("Error: Failed sending message to ", v.Remote, ": ", err) } } // Reply with the list of monitored feeds for `feeds`. case "feeds": var feedList string for _, feed := range feeds { // Add next feed element and a newline. feedList = feedList + feed + "\n" } reply := "Feeds I'm following:\n" + feedList if v.Type == "groupchat" { _, err = client.Send(xmpp.Chat{Remote: muc, Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] + ": " + reply}) if err != nil { log.Fatal("Error: Failed sending message to MUC:", err) } } else if v.Type == "chat" { _, err = client.Send(xmpp.Chat{Remote: v.Remote, Type: "chat", Text: reply}) if err != nil { log.Fatal("Error: Failed sending message to ", v.Remote, ": ", err) } } case "ping": reply := "pong" if v.Type == "groupchat" { _, err = client.Send(xmpp.Chat{Remote: muc, Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] + ": " + reply}) if err != nil { log.Fatal("Error: Failed sending message to MUC:", err) } } else if v.Type == "chat" { _, err = client.Send(xmpp.Chat{Remote: v.Remote, Type: "chat", Text: reply}) if err != nil { log.Fatal("Error: Failed sending message to ", v.Remote, ": ", err) } } case "contact": if contact == "" { contact = "Sorry, no contact information provided." } if v.Type == "groupchat" { _, err = client.Send(xmpp.Chat{Remote: muc, Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] + ": " + contact}) if err != nil { log.Fatal("Error: Failed sending message to MUC:", err) } } else if v.Type == "chat" { _, err = client.Send(xmpp.Chat{Remote: v.Remote, Type: "chat", Text: contact}) if err != nil { log.Fatal("Error: Failed sending message to ", v.Remote, ": ", err) } } } // Reply to pings and disco queries. case xmpp.IQ: if (v.Type == "error") && (v.ID == id) { log.Fatal("MUC not available.") } if (v.Type == "result") && (v.ID == id) { pingReceived = true } if v.Type == "get" { // Reply to disco#info requests according to https://xmpp.org/extensions/xep-0030.html. if strings.Contains(string(v.Query), "") { _, err := client.RawInformation(client.JID(), v.From, v.ID, "result", ""+ ""+ "") if err != nil { log.Fatal("Error: Failed to reply to disco#info:", err) } } else if strings.Contains(string(v.Query), "") { // Reply to pings. _, err := client.RawInformation(client.JID(), v.From, v.ID, "result", "") if err != nil { log.Fatal("Error: Failed to reply to ping:", err) } } else { // Send error replies for all other IQs. _, err := client.RawInformation(client.JID(), v.From, v.ID, "error", "") if err != nil { log.Fatal("Error: Failed to send error IQ:", err) } } } default: break } } }