feed-to-muc/feed-to-muc.go

264 lines
6.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
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
mucStatus, err := client.JoinMUCNoHistory(config.Muc, config.MucNick)
if err != nil {
log.Fatal(err)
}
// Exit if Status is > 300, see https://xmpp.org/registrar/mucstatus.html
if mucStatus > 300 {
os.Exit(mucStatus)
}
// 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)
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)
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(30 * 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) {
for { // Receive stanzas. ToDo: Receive stanzas continiously without 30s delay.
stanza, err := client.Recv()
if err != nil {
log.Fatal(err)
}
// Check IQs for ping results and disco#info queries.
switch v := stanza.(type) {
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
}
}
}