Republish Discord announcements to Abner's Matrix server

This commit is contained in:
Ben Visness 2023-12-05 23:55:02 -06:00
parent 594860a080
commit 76be9b668a
3 changed files with 141 additions and 7 deletions

View File

@ -28,6 +28,7 @@ type HMNConfig struct {
DigitalOcean DigitalOceanConfig
Discord DiscordConfig
Twitch TwitchConfig
Matrix MatrixConfig
EpisodeGuide EpisodeGuide
DevConfig DevConfig
PreviewGeneration PreviewGenerationConfig
@ -93,6 +94,13 @@ type TwitchConfig struct {
BaseIDUrl string
}
type MatrixConfig struct {
Username string
Password string
BaseUrl string
AnnouncementsRoomID string
}
type EpisodeGuide struct {
CineraOutputPath string
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic

View File

@ -1,13 +1,17 @@
package discord
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
@ -19,6 +23,7 @@ import (
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/parsing"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/google/uuid"
)
@ -36,6 +41,10 @@ func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
deleted, err = CleanUpShowcase(ctx, dbConn, msg)
}
if !deleted && err == nil {
err = ShareToMatrix(ctx, msg)
}
if !deleted && err == nil {
err = MaybeInternMessage(ctx, dbConn, msg)
}
@ -137,6 +146,97 @@ func CleanUpLibrary(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (bool
return deleted, nil
}
func ShareToMatrix(ctx context.Context, msg *Message) error {
if msg.Flags&MessageFlagCrossposted == 0 {
return nil
}
if config.Config.Matrix.Username == "" {
logging.ExtractLogger(ctx).Warn().Msg("No Matrix user provided; Discord announcement will not be shared")
}
fullMsg, err := GetChannelMessage(ctx, msg.ChannelID, msg.ID)
if err != nil {
return oops.New(err, "failed to get published message contents")
}
bodyMarkdown := CleanUpMarkdown(ctx, fullMsg.Content)
bodyHTML := parsing.ParseMarkdown(bodyMarkdown, parsing.DiscordMarkdown)
// Log in to Matrix (we don't bother to keep access tokens around)
var accessToken string
{
type MatrixLogin struct {
Type string `json:"type"`
User string `json:"user"`
Password string `json:"password"`
}
type MatrixLoginResponse struct {
AccessToken string `json:"access_token"`
}
body := MatrixLogin{
Type: "m.login.password",
User: config.Config.Matrix.Username,
Password: config.Config.Matrix.Password,
}
bodyBytes := utils.Must1(json.Marshal(body))
res, err := http.Post(
"https://matrix.handmadecities.com/_matrix/client/r0/login",
"application/json",
bytes.NewReader(bodyBytes),
)
if err != nil || res.StatusCode >= 300 {
return oops.New(err, "failed to log into Matrix")
}
defer res.Body.Close()
resBodyBytes := utils.Must1(io.ReadAll(res.Body))
var resBody MatrixLoginResponse
utils.Must(json.Unmarshal(resBodyBytes, &resBody))
accessToken = resBody.AccessToken
}
// Create message
{
type MessageEvent struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
Format string `json:"format,omitempty"`
FormattedBody string `json:"formatted_body,omitempty"`
}
tid := "hmn" + strconv.Itoa(rand.Int())
body := MessageEvent{
MsgType: "m.text",
Body: bodyMarkdown,
Format: "org.matrix.custom.html",
FormattedBody: bodyHTML,
}
bodyBytes := utils.Must1(json.Marshal(body))
req := utils.Must1(http.NewRequestWithContext(
ctx,
http.MethodPut,
fmt.Sprintf(
"%s/_matrix/client/v3/rooms/%s/send/m.room.message/%s",
config.Config.Matrix.BaseUrl,
config.Config.Matrix.AnnouncementsRoomID,
tid,
),
bytes.NewReader(bodyBytes),
))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
req.Header.Add("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil || res.StatusCode >= 300 {
return oops.New(err, "failed to send Matrix message")
}
}
logging.ExtractLogger(ctx).Info().
Str("contents", bodyMarkdown).
Msg("Published Discord announcement to Matrix")
return nil
}
func MaybeInternMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message) error {
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
err := InternMessage(ctx, dbConn, msg)

View File

@ -244,15 +244,32 @@ const (
MessageTypeGuildInviteReminder MessageType = 22
)
type MessageFlags int
const (
MessageFlagCrossposted MessageFlags = 1 << iota
MessageFlagIsCrosspost
MessageFlagSuppressEmbeds
MessageFlagSourceMessageDeleted
MessageFlagUrgent
MessageFlagHasThread
MessageFlagEphemeral
MessageFlagLoading
MessageFlagFailedToMentionSomeRolesInThread
MessageFlagSuppressNotifications
MessageFlagIsVoiceMessage
)
// https://discord.com/developers/docs/resources/channel#message-object
type Message struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
GuildID *string `json:"guild_id"`
Content string `json:"content"`
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
Timestamp string `json:"timestamp"`
Type MessageType `json:"type"`
ID string `json:"id"`
ChannelID string `json:"channel_id"`
GuildID *string `json:"guild_id"`
Content string `json:"content"`
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
Timestamp string `json:"timestamp"`
Type MessageType `json:"type"`
Flags MessageFlags `json:"flags"`
Attachments []Attachment `json:"attachments"`
Embeds []Embed `json:"embeds"`
@ -317,6 +334,7 @@ func MessageFromMap(m interface{}, k string) *Message {
Author: UserFromMap(m, "author"),
Timestamp: maybeString(mmap, "timestamp"),
Type: MessageType(maybeInt(mmap, "type")),
Flags: MessageFlags(maybeInt(mmap, "flags")),
originalMap: mmap,
}
@ -1003,3 +1021,11 @@ func maybeBoolP(m map[string]interface{}, k string) *bool {
boolval := val.(bool)
return &boolval
}
func maybeArray(m map[string]any, k string) []any {
val, ok := m[k]
if !ok || val == nil {
return nil
}
return val.([]any)
}