Basic support of XEP-0045 to send messages to MUC

This commit is contained in:
Alexandre Blazart 2023-10-24 00:43:56 +02:00
parent 6ee09c3994
commit 2215e428a5
No known key found for this signature in database
GPG Key ID: 7067AE298F0C655B
4 changed files with 93 additions and 17 deletions

View File

@ -36,7 +36,9 @@ This example of configuration file shows:
- when it is working, it has the status `Monitoring Prometheus...`
- it doesn't use a TLS socket due to the `no_tls` flag. Actually it will use STARTTLS due to the server configuration
- it doesn't check the TLS certificates thanks to `tls_insecure` (for some reason, it doesn't work on my Prosody install, but as I'm connecting to localhost, it doesn't matter)
- each time it receives an alert, it sends a notification to 2 XMPP accounts `on-duty-1@example.com` and `on-duty-2@example.com`.
- each time it receives an alert, it sends a notification to
- 2 XMPP accounts `on-duty-1@example.com` and `on-duty-2@example.com`
- 1 MUC `monitoring-room-id@conference.example.com` using the nick `monitoring-bot`
```json
{
@ -54,6 +56,12 @@ This example of configuration file shows:
"send_notif": [
"on-duty-1@example.com",
"on-duty-2@example.com"
],
"send_muc": [
{
"room": "monitoring-room-id@conference.example.com",
"nick": "monitoring-bot"
}
]
}
}
@ -82,6 +90,6 @@ This program uses HTTP with 3 different paths:
- `/alert` is used by Prometheus' Alertmanager to send alerts
- `/send` is mainly used for debugging or if one just want to send simple message from another program. To send a message:
- `curl -H 'Content-Type: text/plain' -X POST <my_ip:port>/send -d 'my message'`
- `curl -H 'Content-Type: text/plain' -X POST <my_ip:port>/send -d 'my message'`
- `curl -H 'Content-Type: text/html' -X POST <my_ip:port>/send -d '<p style="color:green;font-weight:bold;">Green text</p>'` if the client supports the deprecated XEP-0071
- `/metrics` to be scrapped by Prometheus. It exposes some basic metrics

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"os"
"strings"
"text/template"
)
@ -20,13 +21,21 @@ type Config struct {
// ConfigXMPP is the configuration for XMPP connection
type ConfigXMPP struct {
OverrideServer string `json:"override_server"`
User string `json:"user"`
Password string `json:"password"`
SendNotif []string `json:"send_notif"`
Status string `json:"status"`
NoTLS bool `json:"no_tls"`
TLSInsecure bool `json:"tls_insecure"`
OverrideServer string `json:"override_server"`
User string `json:"user"`
Password string `json:"password"`
SendNotif []string `json:"send_notif"`
SendMUC []*ConfigMUC `json:"send_muc"`
Status string `json:"status"`
NoTLS bool `json:"no_tls"`
TLSInsecure bool `json:"tls_insecure"`
}
// ConfigMUC is the list of MUC to join (xep-0045)
type ConfigMUC struct {
Room string `json:"room"`
Nick string `json:"nick"`
Password *string `json:"password"`
}
// NewConfig reads the JSON file filename and generates a configuration
@ -47,6 +56,13 @@ func NewConfig(filename string) *Config {
log.Fatalln(err)
}
// default nick
for _, configMUC := range config.XMPP.SendMUC {
if configMUC.Nick == "" {
configMUC.Nick = strings.Split(config.XMPP.User, "@")[0]
}
}
return config
}

View File

@ -36,7 +36,7 @@ var (
Namespace: promNamespace,
Name: "messages_sent_total",
Help: "Number of messages sent.",
}, []string{"recipient", "format"})
}, []string{"recipient", "recipient_type", "format"})
promMessagesReceivedMetric = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: promNamespace,
Name: "messages_received_total",

66
xmpp.go
View File

@ -27,6 +27,7 @@ type xmpp struct {
channel chan xmppMessage
status string
sendNotif []string
sendMUC []string
}
type xmppMessage struct {
@ -35,6 +36,35 @@ type xmppMessage struct {
format Format
}
type xmppChatType int
const (
xmppChatType_Chat xmppChatType = iota
xmppChatType_GroupChat
)
func (x xmppChatType) String() string {
switch x {
case xmppChatType_GroupChat:
return "groupchat"
case xmppChatType_Chat:
return "chat"
default:
return "chat"
}
}
func xmppChatTypeFrom(s string) (xmppChatType, error) {
switch s {
case xmppChatType_Chat.String():
return xmppChatType_Chat, nil
case xmppChatType_GroupChat.String():
return xmppChatType_GroupChat, nil
default:
return xmppChatType_Chat, fmt.Errorf("unhandled chat type: %s", s)
}
}
// NewXMPP create an XMPP connection. Use Close() to end it
func NewXMPP(config *Config) SendCloser {
if config.XMPP.OverrideServer != "" {
@ -69,7 +99,22 @@ func NewXMPP(config *Config) SendCloser {
status: config.XMPP.Status,
sendNotif: config.XMPP.SendNotif,
}
for _, muc := range config.XMPP.SendMUC {
var err error
result.sendMUC = append(result.sendMUC, muc.Room)
if muc.Password != nil {
_, err = client.JoinProtectedMUC(muc.Room, muc.Nick, *muc.Password, libxmpp.NoHistory, 0, nil)
} else {
_, err = client.JoinMUC(muc.Room, muc.Nick, libxmpp.NoHistory, 0, nil)
}
if err != nil {
log.Fatalf("Could not connect to MUC: %s", err)
}
}
prometheus.MustRegister(result)
go result.runSender()
go result.runReceiver()
if config.StartupMessage != "" {
@ -112,10 +157,13 @@ func (x *xmpp) sendTo(to, message string) error {
func (x *xmpp) runSender() {
for payload := range x.channel {
if payload.to != nil {
x.sendToImmediate(*payload.to, payload.message, payload.format)
x.sendToImmediate(xmppChatType_Chat, *payload.to, payload.message, payload.format)
} else {
for _, sendNotif := range x.sendNotif {
x.sendToImmediate(sendNotif, payload.message, payload.format)
x.sendToImmediate(xmppChatType_Chat, sendNotif, payload.message, payload.format)
}
for _, room := range x.sendMUC {
x.sendToImmediate(xmppChatType_GroupChat, room, payload.message, payload.format)
}
}
}
@ -134,7 +182,10 @@ func (x *xmpp) runReceiver() {
x.debug("Stanza: %v\n", stanza)
switch v := stanza.(type) {
case libxmpp.Chat:
x.handleChat(&v)
chatType, err := xmppChatTypeFrom(v.Type)
if err == nil && chatType == xmppChatType_Chat {
x.handleChat(&v)
}
case libxmpp.Presence:
x.handlePresence(&v)
}
@ -160,7 +211,6 @@ func (x *xmpp) handleChat(chat *libxmpp.Chat) {
func (x *xmpp) handlePresence(presence *libxmpp.Presence) {
switch presence.Type {
case "":
case "unavailable":
// something puts us as unavailable
if presence.From == x.client.JID() {
@ -174,6 +224,8 @@ func (x *xmpp) handlePresence(presence *libxmpp.Presence) {
x.client.RevokeSubscription(presence.From)
x.debug("Revoked subscription to %s\n", presence.From)
}
case "error":
fmt.Printf("Error from %s", presence.From)
default:
x.debug("Unhandled presence: %v\n", presence)
}
@ -199,11 +251,11 @@ func (x *xmpp) handleCommand(from, command string) {
}
}
func (x *xmpp) sendToImmediate(to, message string, format Format) {
promMessagesSentMetric.WithLabelValues(to, format.String()).Inc()
func (x *xmpp) sendToImmediate(chatType xmppChatType, to, message string, format Format) {
promMessagesSentMetric.WithLabelValues(to, chatType.String(), format.String()).Inc()
_, err := x.sendChat(libxmpp.Chat{
Remote: to,
Type: "chat",
Type: chatType.String(),
Text: message,
}, format)
if err != nil {