Distributed the code on several files to get a better overview.

This commit is contained in:
Martin Dosch 2019-05-31 23:45:29 +02:00
parent 8ac7a60770
commit 62dfe9d3ac
6 changed files with 360 additions and 330 deletions

View file

@ -4,112 +4,43 @@ Licensed under the "MIT License" */
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"log" "log"
"os"
"os/user"
"strings"
"time" "time"
"github.com/chilts/sid"
"github.com/mattn/go-xmpp" "github.com/mattn/go-xmpp"
) )
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
}
// Variables defined globally as they are used by functions pingMUC // Variables defined globally as they are used by functions pingMUC
// and processStanzas. // and processStanzas.
var ( var (
id string ID string
err error
pingSent time.Time pingSent time.Time
pingReceived bool pingReceived bool
) )
func main() { 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. // Read path to config from command line option.
configFilePtr := flag.String("config", "none", "path to configuration file") configFilePtr := flag.String("config", "none", "path to configuration file")
flag.Parse() flag.Parse()
if *configFilePtr != "none" { config := openConfig(configFilePtr)
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 var client *xmpp.Client
@ -123,13 +54,13 @@ func main() {
} }
// Connect to server. // Connect to server.
client, err = options.NewClient() client, err := options.NewClient()
if err != nil { if err != nil {
log.Fatal("Error: Can't connect to xmpp server: ", err) log.Fatal("Error: Can't connect to xmpp server: ", err)
} }
// Join the MUC // Join the MUC
_, err := client.JoinMUCNoHistory(config.Muc, config.MucNick) _, err = client.JoinMUCNoHistory(config.Muc, config.MucNick)
if err != nil { if err != nil {
log.Fatal("Error: Can't join MUC: ", err) log.Fatal("Error: Can't join MUC: ", err)
} }
@ -172,245 +103,3 @@ func main() {
time.Sleep(config.RefreshTime * time.Second) 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", "<ping xmlns='urn:xmpp:ping'/>")
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 == false) {
time.Sleep(1 * time.Second)
if pingReceived == true {
break
}
}
// Quit if no ping reply was received.
if pingReceived == false {
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 == true {
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.
} 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),
"<query xmlns='http://jabber.org/protocol/disco#info'/>") == true {
_, err := client.RawInformation(client.JID(), v.From, v.ID,
"result", "<query xmlns='http://jabber.org/protocol/disco#info'>"+
"<identity category='client' type='bot' name='feedbot'/>"+
"<feature var='http://jabber.org/protocol/disco#info'/></query>")
if err != nil {
log.Fatal("Error: Failed to reply to disco#info:", err)
}
} else if strings.Contains(string(v.Query), "<ping xmlns='urn:xmpp:ping'/>") == true {
// 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",
"<error type='cancel'><service-unavailable "+
"xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>")
if err != nil {
log.Fatal("Error: Failed to send error IQ:", err)
}
}
}
default:
break
}
}
}

76
openConfig.go Normal file
View file

@ -0,0 +1,76 @@
package main
import (
"encoding/json"
"log"
"os"
"os/user"
)
func openConfig(configFilePtr *string) configuration {
var (
configFile string
configPath string
)
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)
}
// Get home directory.
home := curUser.HomeDir
if home == "" {
log.Fatal("Error: No home directory available.")
}
// 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)
}
}
return config
}

209
processStanzas.go Normal file
View file

@ -0,0 +1,209 @@
package main
import (
"log"
"strings"
"github.com/mattn/go-xmpp"
)
func processStanzas(client *xmpp.Client, muc string, mucNick string, feeds []string, quiet bool, contact string) {
for {
// Receive stanzas.
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 == true {
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.
} 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. processStanza")
}
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),
"<query xmlns='http://jabber.org/protocol/disco#info'/>") == true {
_, err := client.RawInformation(client.JID(), v.From, v.ID,
"result", "<query xmlns='http://jabber.org/protocol/disco#info'>"+
"<identity category='client' type='bot' name='feedbot'/>"+
"<feature var='http://jabber.org/protocol/disco#info'/></query>")
if err != nil {
log.Fatal("Error: Failed to reply to disco#info:", err)
}
} else if strings.Contains(string(v.Query), "<ping xmlns='urn:xmpp:ping'/>") == true {
// 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",
"<error type='cancel'><service-unavailable "+
"xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>")
if err != nil {
log.Fatal("Error: Failed to send error IQ:", err)
}
}
}
default:
break
}
}
}

56
sendPings.go Normal file
View file

@ -0,0 +1,56 @@
package main
import (
"log"
"strings"
"time"
"github.com/chilts/sid"
"github.com/mattn/go-xmpp"
)
// 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) {
var err error
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", "<ping xmlns='urn:xmpp:ping'/>")
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 == false) {
time.Sleep(1 * time.Second)
if pingReceived == true {
break
}
}
// Quit if no ping reply was received.
if pingReceived == false {
log.Fatal("MUC not available. pingMUC")
}
}
}
// 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)
}
}
}