Republish Discord announcements to Abner's Matrix server
This commit is contained in:
parent
594860a080
commit
76be9b668a
|
@ -28,6 +28,7 @@ type HMNConfig struct {
|
||||||
DigitalOcean DigitalOceanConfig
|
DigitalOcean DigitalOceanConfig
|
||||||
Discord DiscordConfig
|
Discord DiscordConfig
|
||||||
Twitch TwitchConfig
|
Twitch TwitchConfig
|
||||||
|
Matrix MatrixConfig
|
||||||
EpisodeGuide EpisodeGuide
|
EpisodeGuide EpisodeGuide
|
||||||
DevConfig DevConfig
|
DevConfig DevConfig
|
||||||
PreviewGeneration PreviewGenerationConfig
|
PreviewGeneration PreviewGenerationConfig
|
||||||
|
@ -93,6 +94,13 @@ type TwitchConfig struct {
|
||||||
BaseIDUrl string
|
BaseIDUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixConfig struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
BaseUrl string
|
||||||
|
AnnouncementsRoomID string
|
||||||
|
}
|
||||||
|
|
||||||
type EpisodeGuide struct {
|
type EpisodeGuide struct {
|
||||||
CineraOutputPath string
|
CineraOutputPath string
|
||||||
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic
|
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package discord
|
package discord
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -19,6 +23,7 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"git.handmade.network/hmn/hmn/src/parsing"
|
"git.handmade.network/hmn/hmn/src/parsing"
|
||||||
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,6 +41,10 @@ func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
|
||||||
deleted, err = CleanUpShowcase(ctx, dbConn, msg)
|
deleted, err = CleanUpShowcase(ctx, dbConn, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !deleted && err == nil {
|
||||||
|
err = ShareToMatrix(ctx, msg)
|
||||||
|
}
|
||||||
|
|
||||||
if !deleted && err == nil {
|
if !deleted && err == nil {
|
||||||
err = MaybeInternMessage(ctx, dbConn, msg)
|
err = MaybeInternMessage(ctx, dbConn, msg)
|
||||||
}
|
}
|
||||||
|
@ -137,6 +146,97 @@ func CleanUpLibrary(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (bool
|
||||||
return deleted, nil
|
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 {
|
func MaybeInternMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message) error {
|
||||||
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
|
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
|
||||||
err := InternMessage(ctx, dbConn, msg)
|
err := InternMessage(ctx, dbConn, msg)
|
||||||
|
|
|
@ -244,15 +244,32 @@ const (
|
||||||
MessageTypeGuildInviteReminder MessageType = 22
|
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
|
// https://discord.com/developers/docs/resources/channel#message-object
|
||||||
type Message struct {
|
type Message struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ChannelID string `json:"channel_id"`
|
ChannelID string `json:"channel_id"`
|
||||||
GuildID *string `json:"guild_id"`
|
GuildID *string `json:"guild_id"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
|
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
|
||||||
Timestamp string `json:"timestamp"`
|
Timestamp string `json:"timestamp"`
|
||||||
Type MessageType `json:"type"`
|
Type MessageType `json:"type"`
|
||||||
|
Flags MessageFlags `json:"flags"`
|
||||||
|
|
||||||
Attachments []Attachment `json:"attachments"`
|
Attachments []Attachment `json:"attachments"`
|
||||||
Embeds []Embed `json:"embeds"`
|
Embeds []Embed `json:"embeds"`
|
||||||
|
@ -317,6 +334,7 @@ func MessageFromMap(m interface{}, k string) *Message {
|
||||||
Author: UserFromMap(m, "author"),
|
Author: UserFromMap(m, "author"),
|
||||||
Timestamp: maybeString(mmap, "timestamp"),
|
Timestamp: maybeString(mmap, "timestamp"),
|
||||||
Type: MessageType(maybeInt(mmap, "type")),
|
Type: MessageType(maybeInt(mmap, "type")),
|
||||||
|
Flags: MessageFlags(maybeInt(mmap, "flags")),
|
||||||
|
|
||||||
originalMap: mmap,
|
originalMap: mmap,
|
||||||
}
|
}
|
||||||
|
@ -1003,3 +1021,11 @@ func maybeBoolP(m map[string]interface{}, k string) *bool {
|
||||||
boolval := val.(bool)
|
boolval := val.(bool)
|
||||||
return &boolval
|
return &boolval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maybeArray(m map[string]any, k string) []any {
|
||||||
|
val, ok := m[k]
|
||||||
|
if !ok || val == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return val.([]any)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue