From 76be9b668ab31cd16a2a78a01a90cb2dc18df0ab Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Tue, 5 Dec 2023 23:55:02 -0600 Subject: [PATCH] Republish Discord announcements to Abner's Matrix server --- src/config/types.go | 8 +++ src/discord/message_handling.go | 100 ++++++++++++++++++++++++++++++++ src/discord/payloads.go | 40 ++++++++++--- 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/src/config/types.go b/src/config/types.go index 4332ae0..650e638 100644 --- a/src/config/types.go +++ b/src/config/types.go @@ -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 diff --git a/src/discord/message_handling.go b/src/discord/message_handling.go index 6cbc2da..0ddf750 100644 --- a/src/discord/message_handling.go +++ b/src/discord/message_handling.go @@ -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) diff --git a/src/discord/payloads.go b/src/discord/payloads.go index c103894..4e5e2b6 100644 --- a/src/discord/payloads.go +++ b/src/discord/payloads.go @@ -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) +}