feed-to-muc/feed-to-muc.go
mdosch efe8f1a549 Made some changes to remove multiple linebreaks and (hopefully) made help output look more nice.
Revert "Revert "(Hopefully) made help output look more nice.""

This reverts commit b73f39ef48.
2019-05-26 21:03:02 +02:00

327 lines
8.5 KiB
Go

/* 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
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: ", err)
}
}
} else { // Get the current user.
curUser, err := user.Current()
if err != nil {
log.Fatal("Error: ", 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: ", err)
}
}
}
configFile = configPath + "config.json"
}
// Check that config file is existing.
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Fatal("Error: ", 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: ", err)
}
if _, err := os.Stat(configFile); os.IsNotExist(err) {
err = os.MkdirAll(configPath, 0700)
if err != nil {
log.Fatal("Error: ", 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(err)
}
// Join the MUC
_, err := client.JoinMUCNoHistory(config.Muc, config.MucNick)
if err != nil {
log.Fatal(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)
// 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(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(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", "<ping xmlns='urn:xmpp:ping'/>")
if err != nil {
log.Fatal(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(err)
}
}
}
func processStanzas(client *xmpp.Client, muc string, mucNick string, feeds []string) {
for { // Receive stanzas. ToDo: Receive stanzas continiously without 30s delay.
stanza, err := client.Recv()
if err != nil {
log.Fatal(err)
}
// Check stanzas, maybe we want to reply.
switch v := stanza.(type) {
// Reply to requests for source and feeds.
case xmpp.Chat:
// Get first word of the message and transform it to lower case.
var mention string
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
}
}
// Check for the second word (command).
switch strings.ToLower(strings.Split(v.Text, " ")[1]) {
// Reply with a short summary of available commands for `help`.
case "help":
_, err = client.Send(xmpp.Chat{Remote: muc,
Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] +
": The following commands are available:\n" +
"feeds " + "List feeds I'm following.\n" +
"source " + "Show source code URL."})
if err != nil {
log.Fatal(err)
}
// Reply with repo address for `source`.
case "source":
_, err = client.Send(xmpp.Chat{Remote: muc,
Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] +
": My source can be found at " +
"https://salsa.debian.org/mdosch-guest/feed-to-muc"})
if err != nil {
log.Fatal(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"
}
_, err = client.Send(xmpp.Chat{Remote: muc,
Type: "groupchat", Text: strings.Split(v.Remote, "/")[1] +
": Feeds I'm following:\n" + feedList})
if err != nil {
log.Fatal(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(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(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(err)
}
}
}
default:
break
}
}
}