warawara

XMPP bot for cerca public RSS feed
git clone http://git.permacomputing.net/repos/warawara.git # read-only access
Log | Files | Refs | README

warawara.go (3980B)


      1 /*
      2   warawara
      3 
      4   The Warawara are unborn human souls who reside in the Sea World. Once they
      5   mature, they fly up into the sky to be born as humans. Sometimes they
      6   bring news from questionnable RSS feeds.
      7 
      8 */
      9 
     10 package main
     11 
     12 import (
     13 	"context"
     14 	"crypto/tls"
     15 	"encoding/xml"
     16 	"flag"
     17 	"fmt"
     18 	"net/http"
     19 	"os"
     20 	"time"
     21 
     22 	"mellium.im/sasl"
     23 	"mellium.im/xmpp"
     24 	"mellium.im/xmpp/dial"
     25 	"mellium.im/xmpp/jid"
     26 	"mellium.im/xmpp/muc"
     27 	"mellium.im/xmpp/stanza"
     28 )
     29 
     30 var (
     31 	feedURL     string
     32 	ctx         context.Context
     33 	cancel      context.CancelFunc
     34 	botJID      string
     35 	botServer   string
     36 	botPassword string
     37 	session     *xmpp.Session
     38 	MUCJID      string
     39 )
     40 
     41 type itemXML struct {
     42 	Title       string `xml:"title"`
     43 	Link        string `xml:"link"`
     44 	Description string `xml:"description"`
     45 	Date        string `xml:"pubDate"`
     46 }
     47 
     48 type channelXML struct {
     49 	Items []itemXML `xml:"channel>item"`
     50 }
     51 
     52 type textMessage struct {
     53 	stanza.Message
     54 	Body string `xml:"body"`
     55 }
     56 
     57 func sendMUCMessage(text string) error {
     58 	to := jid.MustParse(MUCJID)
     59 	var msg textMessage = textMessage{
     60 		Message: stanza.Message{
     61 			From: jid.MustParse(botJID),
     62 			To:   to,
     63 			Type: "groupchat"},
     64 		Body: text}
     65 	return session.Encode(ctx, msg)
     66 }
     67 
     68 func joinXMPP() {
     69 	ctx, cancel = context.WithCancel(context.Background())
     70 	//defer cancel()
     71 	var err error
     72 	dialer := dial.Dialer{}
     73 	dialer.Timeout = 10 * time.Second // https://codeberg.org/mellium/xmpp/issues/430
     74 	conn, err := dialer.Dial(ctx, "tcp", jid.MustParse(botJID))
     75 	if err != nil {
     76 		fmt.Println("Error:", botServer, err)
     77 	}
     78 	negotiator := xmpp.NewNegotiator(func(*xmpp.Session, *xmpp.StreamConfig) xmpp.StreamConfig {
     79 		return xmpp.StreamConfig{
     80 			Features: []xmpp.StreamFeature{
     81 				xmpp.StartTLS(&tls.Config{
     82 					ServerName: botServer,
     83 				}),
     84 				xmpp.SASL("", botPassword, sasl.ScramSha256Plus, sasl.ScramSha1Plus, sasl.ScramSha256, sasl.ScramSha1, sasl.Plain),
     85 				xmpp.BindResource(),
     86 			},
     87 		}
     88 	})
     89 	session, err = xmpp.NewSession(context.TODO(), jid.MustParse(botServer), jid.MustParse(botJID), conn, 0, negotiator)
     90 	if err != nil {
     91 		fmt.Println("Error:", botJID, err)
     92 		os.Exit(1)
     93 	}
     94 	fmt.Println("Connected to", botServer, "as", botJID)
     95 	err = session.Send(ctx, stanza.Presence{Type: stanza.AvailablePresence}.Wrap(nil))
     96 	if err != nil {
     97 		fmt.Println("Error:", err)
     98 	}
     99 }
    100 
    101 // TODO: this is in a goroutine so this won't display errors
    102 // and won't exit, need to use channels or something...
    103 func joinMUC() {
    104 	MUC := jid.MustParse(MUCJID + "/warawara")
    105 	mucClient := muc.Client{}
    106 	_, err := mucClient.Join(ctx, MUC, session)
    107 	if err != nil {
    108 		fmt.Println("Error:", err)
    109 		os.Exit(1)
    110 	}
    111 }
    112 
    113 func fetchFeed() {
    114 	pDateOld := ""
    115 	isFirstRun := true
    116 	for {
    117 		fmt.Println("trying to fetch feed")
    118 		if feed, err := http.Get(feedURL); err != nil {
    119 			fmt.Println("Error:", err)
    120 		} else {
    121 			channel := channelXML{}
    122 			if err := xml.NewDecoder(feed.Body).Decode(&channel); err != nil {
    123 				fmt.Println("Error:", err)
    124 			} else if len(channel.Items) != 0 {
    125 				item := channel.Items[0]
    126 				pTitle := item.Title
    127 				pLink := item.Link
    128 				pDescription := item.Description
    129 				pDate := item.Date
    130 				if pDate != pDateOld {
    131 					if isFirstRun {
    132 						isFirstRun = false
    133 						pDateOld = pDate
    134 						continue
    135 					}
    136 					breakingNews := "🗨️ " + pTitle + " " + pDescription + "\n" + pLink
    137 					fmt.Println(breakingNews)
    138 					if err := sendMUCMessage(breakingNews); err != nil {
    139 						fmt.Println("Error: ", err)
    140 					}
    141 					pDateOld = pDate
    142 				}
    143 			}
    144 		}
    145 		time.Sleep(16 * time.Minute) // cerca's limiter kicks in at 15min
    146 	}
    147 }
    148 
    149 func main() {
    150 	flag.StringVar(&feedURL, "url", "", "RSS feed URL")
    151 	flag.StringVar(&botJID, "bot", "", "bot JID")
    152 	flag.StringVar(&botServer, "server", "", "server")
    153 	flag.StringVar(&botPassword, "password", "", "bot JID password")
    154 	flag.StringVar(&MUCJID, "muc", "", "MUC JID")
    155 	flag.Parse()
    156 
    157 	fmt.Println("🫧 w a r a w a r a 🫧\n")
    158 	fmt.Println("ctrl-c for emergency shutdown\n")
    159 
    160 	joinXMPP()
    161 	go joinMUC()
    162 
    163 	fetchFeed()
    164 }