Skip to content

Instantly share code, notes, and snippets.

@dontlaugh
Created April 8, 2024 18:57
Show Gist options
  • Save dontlaugh/715129e8d306a9df7296f6e140d5d5a0 to your computer and use it in GitHub Desktop.
Save dontlaugh/715129e8d306a9df7296f6e140d5d5a0 to your computer and use it in GitHub Desktop.
xmpp connect and join muc
package xmpp
import (
"context"
"encoding/xml"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"mellium.im/sasl"
"mellium.im/xmlstream"
"mellium.im/xmpp"
"mellium.im/xmpp/dial"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
"mellium.im/xmpp/stanza"
)
// MessageBody is a message stanza that contains a body. It is normally used for
// chat messages.
type MessageBody struct {
stanza.Message
Body string `xml:"body"`
}
// Config models the required information to make an XMPP connection.
type Config struct {
// JID embeds a user and server: user@example.com
JID string `json:"jid"`
Password string `json:"password"`
}
// Client connects to an XMPP server and starts listening for bang "!" commands
type Client struct {
Ctx context.Context
conf *Config
session *xmpp.Session
mucClient *muc.Client
hostname string
}
func (c *Client) Start() error {
ctx := context.Background()
log.Println("dialing", c.conf.JID)
j := jid.MustParse(c.conf.JID)
// dd := dial.Dialer{NoLookup: true}
dd := dial.Dialer{NoLookup: true}
conn, err := dd.Dial(ctx, "tcp", j)
if err != nil {
return err
}
log.Println("starting session")
sess, err := xmpp.NewClientSession(ctx, j, conn, xmpp.BindResource(), xmpp.StartTLS(nil),
xmpp.SASL("", c.conf.Password, sasl.ScramSha1, sasl.Plain))
if err != nil {
return err
}
c.session = sess
log.Println("send initial presence")
// Send initial presence to let the server know we want to receive messages.
err = c.session.Send(context.TODO(), stanza.Presence{Type: stanza.AvailablePresence}.Wrap(nil))
if err != nil {
return fmt.Errorf("Error sending initial presence: %q", err)
}
roomJID, err := jid.Parse("system-monitor@rooms.mydomain.local/botnickname")
if err != nil {
return err
}
mucClient := &muc.Client{}
go func() {
log.Println("join muc")
_, err = mucClient.Join(ctx, roomJID, c.session, muc.MaxHistory(0))
if err != nil {
log.Fatalf("Failed to join chatroom: %v", err)
}
}()
c.mucClient = mucClient
log.Println("serve")
// Hmm... passing ourselves in is weird. But Client is what implements ServeXMPP,
// and that's what the session's Serve method wants. Keep an eye on this.
return c.session.Serve(c)
}
func (c *Client) Stop() error {
if err := c.session.Close(); err != nil {
return fmt.Errorf("close session: %w", err)
}
if err := c.session.Conn().Close(); err != nil {
return fmt.Errorf("close connection: %w", err)
}
return nil
}
// NewClient builds a client.
func NewClient(conf *Config) (*Client, error) {
// Build up this client, except for session that gets created in Start
var c Client
c.conf = conf
// Set hostname
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
c.hostname = hostname
return &c, nil
}
func (c *Client) HandleXMPP(t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
// https://codeberg.org/mellium/xmpp/pulls/172/files
d := xml.NewTokenDecoder(xmlstream.MultiReader(xmlstream.Token(*start), t))
d.Token()
// Ignore anything that's not a message. In a real system we'd want to at
// least respond to IQs.
if start.Name.Local != "message" {
return nil
}
var msg MessageBody
err := d.DecodeElement(&msg, start)
if err != nil && err != io.EOF {
log.Printf("Error decoding message: %q", err)
return nil
}
// Ignore unless they are chat messages and have a body.
if msg.Body == "" || msg.Type != stanza.ChatMessage {
return nil
}
// features
// * package update
// * package install
// * uptime
ctx := context.Background()
log.Println("body", msg.Body)
/* take action here */
if strings.HasPrefix(msg.Body, "!") {
splitted := strings.Fields(msg.Body)
if len(splitted) < 1 {
return nil
}
cmdPrefix := strings.TrimPrefix(splitted[0], "!")
var cmd *exec.Cmd
pathToScript := filepath.Join("exec", cmdPrefix)
// fmt.Printf("num args: %v\n", len(splitted))
var args []string
if len(splitted) == 2 {
args = []string{splitted[1]}
cmd = exec.CommandContext(ctx, pathToScript, args...)
} else if len(splitted) > 2 {
args = splitted[1:len(splitted)]
cmd = exec.CommandContext(ctx, pathToScript, args...)
} else {
cmd = exec.CommandContext(ctx, pathToScript)
}
data, err := cmd.CombinedOutput()
if err != nil {
return err
}
reply := MessageBody{
Message: stanza.Message{
To: msg.From.Bare(),
},
Body: strings.TrimSpace(string(data)),
}
if err := t.Encode(reply); err != nil {
log.Printf("Error responding to message %q: %q", msg.ID, err)
return err
}
}
return nil
}
@dontlaugh
Copy link
Author

Logs from this demonstrate 1) a slow dial and 2) a forever-hanging call to Join

./chatops-bot
2024/04/08 15:04:38 dialing mybot@mydomain.local
2024/04/08 15:06:53 starting session
2024/04/08 15:06:54 send initial presence
2024/04/08 15:06:54 serve
2024/04/08 15:06:54 join muc

@dontlaugh
Copy link
Author

Mellium versions

% cat go.mod | grep mellium
	mellium.im/reader v0.1.0 // indirect
	mellium.im/sasl v0.3.1 // indirect
	mellium.im/xmlstream v0.15.4 // indirect
	mellium.im/xmpp v0.21.4 // indirect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment