diff options
| author | Xe Iaso <me@xeiaso.net> | 2023-06-17 19:59:20 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2023-06-17 19:59:20 -0400 |
| commit | 276b1fdd41de087a6700a1578e71dd8ccf83fee6 (patch) | |
| tree | d8e5f2e94333e392ae836d6dd6c77e78ee01f4ce /cmd | |
| parent | 24f5fb57f3e36ff2f6150468c4cb10c4f5bb282c (diff) | |
| download | x-276b1fdd41de087a6700a1578e71dd8ccf83fee6.tar.xz x-276b1fdd41de087a6700a1578e71dd8ccf83fee6.zip | |
cmd/marabot: upload shit to S3
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/marabot/discord.go | 105 | ||||
| -rw-r--r-- | cmd/marabot/irc.go | 41 | ||||
| -rw-r--r-- | cmd/marabot/main.go | 175 | ||||
| -rw-r--r-- | cmd/marabot/revolt.go | 107 | ||||
| -rw-r--r-- | cmd/marabot/schema.sql | 57 | ||||
| -rw-r--r-- | cmd/marabot/terraform/.gitignore | 1 | ||||
| -rw-r--r-- | cmd/marabot/terraform/.terraform.lock.hcl | 25 | ||||
| -rw-r--r-- | cmd/marabot/terraform/main.tf | 89 |
8 files changed, 531 insertions, 69 deletions
diff --git a/cmd/marabot/discord.go b/cmd/marabot/discord.go index d9495df..5ea8966 100644 --- a/cmd/marabot/discord.go +++ b/cmd/marabot/discord.go @@ -4,13 +4,18 @@ import ( "context" "database/sql" "fmt" + "path/filepath" + "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" "github.com/bwmarrin/discordgo" + "github.com/google/uuid" "within.website/ln" "within.website/ln/opname" ) -func importDiscordData(ctx context.Context, db *sql.DB, dg *discordgo.Session) error { +func (mr *MaraRevolt) importDiscordData(ctx context.Context, db *sql.DB, dg *discordgo.Session) error { ctx = opname.With(ctx, "import-discord-data") channels, err := dg.GuildChannels(*furryholeDiscord) if err != nil { @@ -42,10 +47,106 @@ func importDiscordData(ctx context.Context, db *sql.DB, dg *discordgo.Session) e return err } for _, emoji := range emoji { - if _, err := db.ExecContext(ctx, "INSERT INTO discord_emoji (id, guild_id, name, url) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, url = EXCLUDED.url", emoji.ID, furryholeDiscord, emoji.Name, fmt.Sprintf("https://cdn.discordapp.com/emojis/%s.webp?size=240&quality=lossless", emoji.ID)); err != nil { + eURL := fmt.Sprintf("https://cdn.discordapp.com/emojis/%s?size=240&quality=lossless", emoji.ID) + if _, err := db.ExecContext(ctx, "INSERT INTO discord_emoji (id, guild_id, name, url) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, url = EXCLUDED.url", emoji.ID, furryholeDiscord, emoji.Name, eURL); err != nil { ln.Error(ctx, err, ln.Action("inserting emoji")) continue } + mr.attachmentPreprocess.Add([3]string{eURL, "emoji", ""}, len(eURL)) + } + + return nil +} + +func (mr *MaraRevolt) DiscordMessageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { + ctx := opname.With(context.Background(), "marabot.discord-message-delete") + if _, err := mr.db.ExecContext(ctx, "DELETE FROM discord_messages WHERE id = ?", m.ID); err != nil { + ln.Error(ctx, err, ln.Action("nuking deleted messages")) + } + + row := mr.db.QueryRowContext(ctx, "SELECT id FROM s3_uploads WHERE message_id = ?", m.ID) + if row.Err() == nil { + var id string + if err := row.Scan(&id); err != nil { + ln.Error(ctx, err) + return + } + + if _, err := mr.db.ExecContext(ctx, "DELETE FROM s3_uploads WHERE id = ?", id); err != nil { + ln.Error(ctx, err) + } + + if _, err := mr.s3.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{ + Key: aws.String(m.ID), + }); err != nil { + ln.Error(ctx, err) + } + } +} + +func (mr *MaraRevolt) DiscordMessageEdit(s *discordgo.Session, m *discordgo.MessageUpdate) { + if _, err := mr.db.Exec("UPDATE discord_messages SET content = ?, edited_at = ? WHERE id = ?", m.Content, time.Now().Format(time.RFC3339), m.ID); err != nil { + ln.Error(context.Background(), err) + } +} + +func (mr *MaraRevolt) DiscordMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + if err := mr.discordMessageCreate(s, m); err != nil { + ln.Error(context.Background(), err) + } +} + +func (mr *MaraRevolt) discordMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) error { + if _, err := mr.db.Exec(`INSERT INTO discord_users (id, username, avatar_url, accent_color) +VALUES (?, ?, ?, ?) +ON CONFLICT(id) +DO UPDATE SET username = EXCLUDED.username, avatar_url = EXCLUDED.avatar_url, accent_color = EXCLUDED.accent_color`, m.Author.ID, m.Author.Username, m.Author.AvatarURL(""), m.Author.AccentColor); err != nil { + return err + } + + mr.attachmentPreprocess.Add([3]string{m.Author.AvatarURL(""), "avatars", ""}, len(m.Author.Avatar)) + + if _, err := mr.db.Exec(`INSERT INTO discord_messages (id, guild_id, channel_id, author_id, content, created_at, edited_at, webhook_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, m.ID, m.GuildID, m.ChannelID, m.Author.ID, m.Content, m.Timestamp.Format(time.RFC3339), m.EditedTimestamp, m.WebhookID); err != nil { + return err + } + + if m.WebhookID != "" { + if _, err := mr.db.Exec("INSERT INTO discord_webhook_message_info (id, name, avatar_url) VALUES (?, ?, ?)", m.ID, m.Author.Username, m.Author.AvatarURL("")); err != nil { + return err + } + } + + for _, att := range m.Attachments { + if _, err := mr.db.Exec(`INSERT INTO discord_attachments (id, message_id, url, proxy_url, filename, content_type, width, height, size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, att.ID, m.ID, att.URL, att.ProxyURL, att.Filename, att.ContentType, att.Width, att.Height, att.Size); err != nil { + return err + } + + mr.attachmentPreprocess.Add([3]string{att.URL, "attachments", m.ID}, len(att.URL)) + } + + for _, emb := range m.Embeds { + if _, err := mr.db.Exec(`INSERT INTO discord_attachments (id, message_id, url, proxy_url, filename, content_type, width, height, size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, uuid.NewString(), m.ID, emb.Image.URL, emb.Image.ProxyURL, filepath.Base(emb.Image.URL), "", emb.Image.Width, emb.Image.Height, 0); err != nil { + return err + } + + mr.attachmentPreprocess.Add([3]string{emb.Image.URL, "attachments", m.ID}, len(emb.Image.URL)) + } + + ch, err := s.Channel(m.ChannelID) + if err != nil { + return err + } + + if _, err := mr.db.Exec("INSERT INTO discord_channels (id, guild_id, name, topic, nsfw) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, topic = EXCLUDED.topic, nsfw = EXCLUDED.nsfw", ch.ID, ch.GuildID, ch.Name, ch.Topic, ch.NSFW); err != nil { + return err + } + + for _, emoji := range m.GetCustomEmojis() { + eURL := fmt.Sprintf("https://cdn.discordapp.com/emojis/%s?size=240&quality=lossless", emoji.ID) + if _, err := mr.db.Exec("INSERT INTO discord_emoji (id, guild_id, name, url) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, url = EXCLUDED.url", emoji.ID, furryholeDiscord, emoji.Name, eURL); err != nil { + return err + } + mr.attachmentPreprocess.Add([3]string{eURL, "emoji", ""}, len(eURL)) } return nil diff --git a/cmd/marabot/irc.go b/cmd/marabot/irc.go index 51479c6..fb1010a 100644 --- a/cmd/marabot/irc.go +++ b/cmd/marabot/irc.go @@ -2,24 +2,27 @@ package main import ( "context" - "database/sql" "flag" + "time" irc "github.com/thoj/go-ircevent" "within.website/ln" "within.website/ln/opname" + "within.website/x/internal" + "within.website/x/web/revolt" ) var ( - ircNick = flag.String("irc-nick", "[Mara]", "IRC nickname") - ircUser = flag.String("irc-user", "sh0rk", "IRC username") - ircReal = flag.String("irc-real", "Friendly sh0rk Mara", "IRC realname") - ircServer = flag.String("irc-server", "chrysalis:6667", "IRC server to connect to") - ircSASLUsername = flag.String("irc-sasl-username", "", "SASL username") - ircSASLPassword = flag.String("irc-sasl-password", "", "SASL password") + ircNick = flag.String("irc-nick", "[Mara]", "IRC nickname") + ircUser = flag.String("irc-user", "sh0rk", "IRC username") + ircReal = flag.String("irc-real", "Friendly sh0rk Mara", "IRC realname") + ircServer = flag.String("irc-server", "chrysalis:6667", "IRC server to connect to") + ircSASLUsername = flag.String("irc-sasl-username", "", "SASL username") + ircSASLPassword = flag.String("irc-sasl-password", "", "SASL password") + ircRevoltChannel = flag.String("irc-revolt-channel", "", "channel to copy #xeserv messages to") ) -func NewIRCBot(ctx context.Context, db *sql.DB, messages chan string) { +func (mr *MaraRevolt) IRCBot(ctx context.Context) { ctx = opname.With(ctx, "ircbot") ctx = ln.WithF(ctx, ln.F{ "irc_server": *ircServer, @@ -32,11 +35,14 @@ func NewIRCBot(ctx context.Context, db *sql.DB, messages chan string) { }() go func() { + t := time.NewTicker(250 * time.Millisecond) + defer t.Stop() for { select { case <-ctx.Done(): return - case msg := <-messages: + case msg := <-mr.ircmsgs: + <-t.C irccon.Privmsg("#xeserv", msg) } } @@ -51,9 +57,24 @@ func NewIRCBot(ctx context.Context, db *sql.DB, messages chan string) { irccon.AddCallback("001", func(e *irc.Event) { irccon.Join("#xeserv") }) irccon.AddCallback("PRIVMSG", func(e *irc.Event) { - if _, err := db.ExecContext(ctx, `INSERT INTO irc_messages(nick, user, host, channel, content, tags) VALUES (?, ?, ?, ?, ?, ?)`, e.Nick, e.User, e.Host, e.Arguments[0], e.Message(), ""); err != nil { + if _, err := mr.db.ExecContext(ctx, `INSERT INTO irc_messages(nick, user, host, channel, content, tags) VALUES (?, ?, ?, ?, ?, ?)`, e.Nick, e.User, e.Host, e.Arguments[0], e.Message(), ""); err != nil { ln.Error(ctx, err) } + + if e.Arguments[0] == "#xeserv" { + sendMsg := &revolt.SendMessage{ + Masquerade: &revolt.Masquerade{ + Name: e.Nick, + AvatarURL: "https://cdn.xeiaso.net/avatar/" + internal.Hash(e.User, e.Host), + }, + Content: e.Message(), + } + + if _, err := mr.cli.ChannelSendMessage(ctx, *ircRevoltChannel, sendMsg); err != nil { + ln.Error(ctx, err) + return + } + } }) err := irccon.Connect(*ircServer) if err != nil { diff --git a/cmd/marabot/main.go b/cmd/marabot/main.go index 3c5511a..bbbec9a 100644 --- a/cmd/marabot/main.go +++ b/cmd/marabot/main.go @@ -1,22 +1,33 @@ package main import ( + "bytes" "context" + "crypto/sha512" "database/sql" _ "embed" "flag" "fmt" + "io" + "net/http" "os" "os/signal" + "path/filepath" "syscall" "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/bwmarrin/discordgo" _ "modernc.org/sqlite" "tailscale.com/hostinfo" "within.website/ln" "within.website/ln/opname" "within.website/x/internal" + "within.website/x/internal/bundler" + "within.website/x/web" "within.website/x/web/revolt" ) @@ -26,6 +37,7 @@ var ( revoltToken = flag.String("revolt-token", "", "Revolt bot token") revoltAPIServer = flag.String("revolt-api-server", "https://api.revolt.chat", "API server for Revolt") revoltWebsocketServer = flag.String("revolt-ws-server", "wss://ws.revolt.chat", "Websocket server for Revolt") + revoltBotID = flag.String("revolt-bot-id", "", "bot ID for revolt") tsAuthkey = flag.String("ts-authkey", "", "Tailscale authkey") tsHostname = flag.String("ts-hostname", "", "Tailscale hostname") @@ -34,6 +46,8 @@ var ( furryholeDiscord = flag.String("furryhole-discord", "192289762302754817", "Discord channel ID for furryhole") furryholeRevolt = flag.String("furryhole-revolt", "01FEXZ1XPWMEJXMF836FP16HB8", "Revolt channel ID for furryhole") + awsS3Bucket = flag.String("aws-s3-bucket", "", "S3 bucket name") + awsS3Region = flag.String("aws-s3-region", "ca-central-1", "S3 bucket region") //go:embed schema.sql dbSchema string @@ -59,9 +73,7 @@ func main() { ln.FatalErr(ctx, err, ln.Action("running database schema")) } - ircmsgs := make(chan string) - - go NewIRCBot(ctx, db, ircmsgs) + ircmsgs := make(chan string, 10) // Init a new client. client, err := revolt.NewWithEndpoint(*revoltToken, *revoltAPIServer, *revoltWebsocketServer) @@ -69,11 +81,30 @@ func main() { ln.FatalErr(ctx, err, ln.Action("creating revolt client")) } + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(*awsS3Region)}, + ) + + uploader := s3manager.NewUploader(sess) + mr := &MaraRevolt{ - cli: client, - db: db, + cli: client, + db: db, + ircmsgs: ircmsgs, + uploader: uploader, + s3: s3.New(sess), } + mr.attachmentUpload = bundler.New(mr.S3Upload) + mr.attachmentUpload.BundleCountThreshold = 5 + mr.attachmentUpload.DelayThreshold = time.Minute + + mr.attachmentPreprocess = bundler.New(mr.PreprocessLinks) + mr.attachmentPreprocess.BundleCountThreshold = 10 + mr.attachmentPreprocess.DelayThreshold = 30 * time.Second + + go mr.IRCBot(ctx) + client.Connect(ctx, mr) dg, err := discordgo.New("Bot " + *discordToken) @@ -82,6 +113,7 @@ func main() { } dg.AddHandler(mr.DiscordMessageCreate) + dg.AddHandler(mr.DiscordMessageDelete) dg.AddHandler(mr.DiscordMessageEdit) if err := dg.Open(); err != nil { @@ -89,7 +121,7 @@ func main() { } defer dg.Close() - if err := importDiscordData(ctx, db, dg); err != nil { + if err := mr.importDiscordData(ctx, db, dg); err != nil { ln.Error(ctx, err) } @@ -115,80 +147,111 @@ func main() { } } -func (mr *MaraRevolt) DiscordMessageEdit(s *discordgo.Session, m *discordgo.MessageUpdate) { - if _, err := mr.db.Exec("UPDATE discord_messages SET content = ?, edited_at = ? WHERE id = ?", m.Content, time.Now().Format(time.RFC3339), m.ID); err != nil { - ln.Error(context.Background(), err) - } +type Attachment struct { + ID string `json:"id"` + URL string `json:"url"` + Kind string `json:"kind"` + ContentType string `json:"content_type"` + CreatedAt string `json:"created_at"` + MessageID *string `json:"message_id"` + Data []byte `json:"-"` } -func (mr *MaraRevolt) DiscordMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - if err := mr.discordMessageCreate(s, m); err != nil { - ln.Error(context.Background(), err) - } -} +func (mr *MaraRevolt) PreprocessLinks(data [][3]string) { + ctx := opname.With(context.Background(), "marabot.link-preprocessor") + ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() -func (mr *MaraRevolt) discordMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) error { - if _, err := mr.db.Exec(`INSERT INTO discord_users (id, username, avatar_url, accent_color) -VALUES (?, ?, ?, ?) -ON CONFLICT(id) -DO UPDATE SET username = EXCLUDED.username, avatar_url = EXCLUDED.avatar_url, accent_color = EXCLUDED.accent_color`, m.Author.ID, m.Author.Username, m.Author.AvatarURL(""), m.Author.AccentColor); err != nil { - return err - } + mr.preprocessLinks(ctx, data) +} - if _, err := mr.db.Exec(`INSERT INTO discord_messages (id, guild_id, channel_id, author_id, content, created_at, edited_at, webhook_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, m.ID, m.GuildID, m.ChannelID, m.Author.ID, m.Content, m.Timestamp.Format(time.RFC3339), m.EditedTimestamp, m.WebhookID); err != nil { - return err - } +func (mr *MaraRevolt) preprocessLinks(ctx context.Context, data [][3]string) { + for _, linkkind := range data { + kind := linkkind[1] + link := linkkind[0] + msgID := linkkind[2] - if m.WebhookID != "" { - if _, err := mr.db.Exec("INSERT INTO discord_webhook_message_info (id, name, avatar_url) VALUES (?, ?, ?)", m.ID, m.Author.Username, m.Author.AvatarURL("")); err != nil { - return err + att, err := hashURL(link, kind) + if err != nil { + ln.Error(ctx, err, ln.F{"link": link, "kind": kind}) } - } - for _, att := range m.Attachments { - if _, err := mr.db.Exec(`INSERT INTO discord_attachments (id, message_id, url, proxy_url, filename, content_type, width, height, size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, att.ID, m.ID, att.URL, att.ProxyURL, att.Filename, att.ContentType, att.Width, att.Height, att.Size); err != nil { - return err - } + att.MessageID = aws.String(msgID) + + mr.attachmentUpload.Add(att, len(att.Data)) } +} - ch, err := s.Channel(m.ChannelID) +func hashURL(itemURL, kind string) (*Attachment, error) { + resp, err := http.Get(itemURL) if err != nil { - return err + return nil, err } + if resp.StatusCode != http.StatusOK { + return nil, web.NewError(http.StatusOK, resp) + } + defer resp.Body.Close() - if _, err := mr.db.Exec("INSERT INTO discord_channels (id, guild_id, name, topic, nsfw) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, topic = EXCLUDED.topic, nsfw = EXCLUDED.nsfw", ch.ID, ch.GuildID, ch.Name, ch.Topic, ch.NSFW); err != nil { - return err + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err } - for _, emoji := range m.GetCustomEmojis() { - if _, err := mr.db.Exec("INSERT INTO discord_emoji (id, guild_id, name, url) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, url = EXCLUDED.url", emoji.ID, furryholeDiscord, emoji.Name, fmt.Sprintf("https://cdn.discordapp.com/emojis/%s.webp?size=240&quality=lossless", emoji.ID)); err != nil { - return err - } + h := sha512.New() + h.Write(data) + hash := fmt.Sprintf("%x", h.Sum(nil)) + + result := &Attachment{ + ID: hash, + URL: itemURL, + Kind: kind, + CreatedAt: time.Now().Format(time.RFC3339), + ContentType: resp.Header.Get("Content-Type"), + Data: data, } - return nil + return result, nil } -type MaraRevolt struct { - cli *revolt.Client - db *sql.DB - revolt.NullHandler +func (mr *MaraRevolt) S3Upload(att []*Attachment) { + ctx := opname.With(context.Background(), "marabot.s3-uploader") + ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + + mr.s3Upload(ctx, att) } -func (mr *MaraRevolt) MessageCreate(ctx context.Context, msg *revolt.Message) error { - if msg.Content == "!ping" { - sendMsg := &revolt.SendMessage{ - Masquerade: &revolt.Masquerade{ - Name: "cipra", - AvatarURL: "https://cdn.xeiaso.net/avatar/" + internal.Hash("cipra", "yasomi"), +func (mr *MaraRevolt) s3Upload(ctx context.Context, att []*Attachment) { + for _, att := range att { + key := filepath.Join(att.Kind, att.ID) + + var count int + if err := mr.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM s3_uploads WHERE id = ?", att.ID).Scan(&count); err != nil { + ln.Error(ctx, err) + continue + } + + if count != 0 { + continue + } + + if _, err := mr.uploader.UploadWithContext(ctx, &s3manager.UploadInput{ + Bucket: aws.String(*awsS3Bucket), + Key: aws.String(key), + ContentType: aws.String(att.ContentType), + Body: bytes.NewBuffer(att.Data), + Metadata: map[string]*string{ + "Original-URL": aws.String(att.URL), + "Message-ID": att.MessageID, }, + }); err != nil { + ln.Error(ctx, err, ln.Action("trying to upload to S3"), ln.F{"att_url": att.URL, "att_content_type": att.ContentType}) + continue } - sendMsg.SetContent("🏓 Pong!") - if _, err := mr.cli.MessageReply(ctx, msg.ChannelId, msg.ID, true, sendMsg); err != nil { - return err + if _, err := mr.db.ExecContext(ctx, "INSERT INTO s3_uploads(id, url, kind, content_type, created_at, message_id) VALUES (?, ?, ?, ?, ?, ?)", att.ID, att.URL, att.Kind, att.ContentType, att.CreatedAt, att.MessageID); err != nil { + ln.Error(ctx, err, ln.Action("saving upload information to DB")) } } - return nil } diff --git a/cmd/marabot/revolt.go b/cmd/marabot/revolt.go new file mode 100644 index 0000000..f9bffc0 --- /dev/null +++ b/cmd/marabot/revolt.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go/service/s3/s3iface" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "within.website/ln" + "within.website/x/internal" + "within.website/x/internal/bundler" + "within.website/x/web/revolt" +) + +type MaraRevolt struct { + cli *revolt.Client + db *sql.DB + ircmsgs chan string + attachmentPreprocess *bundler.Bundler[[3]string] + attachmentUpload *bundler.Bundler[*Attachment] + uploader *s3manager.Uploader + s3 s3iface.S3API + + revolt.NullHandler +} + +func (mr *MaraRevolt) MessageCreate(ctx context.Context, msg *revolt.Message) error { + if msg.Content == "!ping" { + sendMsg := &revolt.SendMessage{ + Masquerade: &revolt.Masquerade{ + Name: "cipra", + AvatarURL: "https://cdn.xeiaso.net/avatar/" + internal.Hash("cipra", "yasomi"), + }, + } + sendMsg.SetContent("🏓 Pong!") + + if _, err := mr.cli.MessageReply(ctx, msg.ChannelId, msg.ID, true, sendMsg); err != nil { + return err + } + } + + if err := msg.CalculateCreationDate(); err != nil { + return err + } + + if _, err := mr.db.ExecContext(ctx, "INSERT INTO revolt_messages(id, channel_id, author_id, content, created_at) VALUES (?, ?, ?, ?, ?)", msg.ID, msg.ChannelId, msg.AuthorId, msg.Content, msg.CreatedAt.Format(time.RFC3339)); err != nil { + ln.Error(ctx, err, ln.Action("saving revolt message to database")) + } + + if msg.Masquerade != nil { + if _, err := mr.db.ExecContext(ctx, "INSERT INTO revolt_message_masquerade(id, username, avatar_url) VALUES(?, ?, ?)", msg.ID, msg.Masquerade.Name, msg.Masquerade.AvatarURL); err != nil { + ln.Error(ctx, err, ln.Action("saving masquerade info to database")) + } + + mr.attachmentPreprocess.Add([3]string{msg.Masquerade.AvatarURL, "avatars", ""}, len(msg.Masquerade.AvatarURL)) + } + + author, err := mr.cli.FetchUser(ctx, msg.AuthorId) + if err != nil { + return err + } + if err := author.CalculateCreationDate(); err != nil { + return err + } + avatarURL := mr.cli.ResolveAttachment(author.Avatar) + + if _, err := mr.db.ExecContext(ctx, "INSERT INTO revolt_users(id, username, avatar_url, created_at) VALUES(?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET username = EXCLUDED.username, avatar_url = EXCLUDED.avatar_url, created_at = EXCLUDED.created_at", author.Id, author.Username, avatarURL, author.CreatedAt.Format(time.RFC3339)); err != nil { + ln.Error(ctx, err, ln.Action("writing revolt user record")) + } + + for _, att := range msg.Attachments { + url := mr.cli.ResolveAttachment(att) + + if _, err := mr.db.ExecContext(ctx, "INSERT INTO revolt_attachments(id, tag, message_id, url, filename, content_type, width, height, size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", att.ID, att.Tag, msg.ID, url, att.FileName, att.ContentType, att.Metadata.Width, att.Metadata.Height, att.Size); err != nil { + ln.Error(ctx, err, ln.Action("writing revolt attachment information")) + } + + mr.attachmentPreprocess.Add([3]string{url, "attachments", msg.ID}, len(url)) + } + + if msg.ChannelId == *ircRevoltChannel && msg.AuthorId != *revoltBotID { + var nick string = author.Username + + if msg.Masquerade != nil { + nick = msg.Masquerade.Name + } + + if !strings.Contains(msg.Content, "\n") { + if msg.Content != "" { + mr.ircmsgs <- fmt.Sprintf("%s\\ %s", nick, msg.Content) + } + } else { + for _, line := range strings.Split(msg.Content, "\n") { + mr.ircmsgs <- fmt.Sprintf("%s\\ %s", nick, line) + } + } + + for _, att := range msg.Attachments { + mr.ircmsgs <- fmt.Sprintf("%s\\ %s", nick, mr.cli.ResolveAttachment(att)) + } + } + + return nil +} diff --git a/cmd/marabot/schema.sql b/cmd/marabot/schema.sql index 05a7f56..f38537f 100644 --- a/cmd/marabot/schema.sql +++ b/cmd/marabot/schema.sql @@ -78,6 +78,13 @@ CREATE TABLE IF NOT EXISTS irc_messages ( tags TEXT NOT NULL ); +CREATE TABLE IF NOT EXISTS revolt_channels ( + id TEXT PRIMARY KEY, + server_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL +); + CREATE TABLE IF NOT EXISTS revolt_emoji ( id TEXT PRIMARY KEY, server_id TEXT NOT NULL, @@ -89,4 +96,52 @@ CREATE TABLE IF NOT EXISTS revolt_discord_emoji ( revolt_id TEXT NOT NULL, discord_id TEXT NOT NULL, name TEXT NOT NULL -);
\ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS revolt_messages ( + id TEXT PRIMARY KEY, + channel_id TEXT NOT NULL, + author_id TEXT NOT NULL, + content TEXT NOT NULL, + created_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS revolt_message_masquerade ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + avatar_url TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS revolt_servers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS revolt_users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + avatar_url TEXT NOT NULL, + created_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS revolt_attachments ( + id TEXT PRIMARY KEY, + tag TEXT NOT NULL, + message_id TEXT, + url TEXT NOT NULL, + filename TEXT NOT NULL, + content_type TEXT NOT NULL, + width INTEGER, + height INTEGER, + "size" INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS s3_uploads ( + id TEXT PRIMARY KEY, -- sha512 of file contents + url TEXT NOT NULL, + kind TEXT NOT NULL, + content_type TEXT NOT NULL, + created_at TEXT NOT NULL, + message_id TEXT +); diff --git a/cmd/marabot/terraform/.gitignore b/cmd/marabot/terraform/.gitignore new file mode 100644 index 0000000..3fa8c86 --- /dev/null +++ b/cmd/marabot/terraform/.gitignore @@ -0,0 +1 @@ +.terraform diff --git a/cmd/marabot/terraform/.terraform.lock.hcl b/cmd/marabot/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..78d3c55 --- /dev/null +++ b/cmd/marabot/terraform/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "4.67.0" + constraints = "~> 4.0" + hashes = [ + "h1:dCRc4GqsyfqHEMjgtlM1EympBcgTmcTkWaJmtd91+KA=", + "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060", + "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6", + "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183", + "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1", + "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29", + "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7", + "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043", + "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362", + "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf", + "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b", + "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c", + "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c", + "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d", + ] +} diff --git a/cmd/marabot/terraform/main.tf b/cmd/marabot/terraform/main.tf new file mode 100644 index 0000000..99a4084 --- /dev/null +++ b/cmd/marabot/terraform/main.tf @@ -0,0 +1,89 @@ +terraform { + backend "s3" { + bucket = "within-tf-state" + key = "aws/marabot" + region = "us-east-1" + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + } +} + +resource "aws_s3_bucket" "marabot" { + bucket = "xeserv-marabot" + + tags = { + Name = "Marabot uploads" + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "marabot" { + bucket = aws_s3_bucket.marabot.id + + rule { + id = "auto_glacier_backup" + status = "Enabled" + + transition { + days = 90 + storage_class = "GLACIER" + } + + filter { + prefix = "attachments/" + } + } +} + +data "aws_iam_policy_document" "marabot" { + statement { + actions = [ + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket", + ] + effect = "Allow" + resources = [ + aws_s3_bucket.marabot.arn, + "${aws_s3_bucket.marabot.arn}/*", + ] + } + + statement { + actions = ["s3:ListAllMyBuckets"] + effect = "Allow" + resources = ["*"] + } +} + +resource "aws_iam_policy" "marabot" { + name = "marabot-policy" + description = "policy for managing S3 for marabot" + + policy = data.aws_iam_policy_document.marabot.json +} + +resource "aws_iam_user" "marabot" { + name = "marabot" + path = "/within/marabot/" +} + +resource "aws_iam_user_policy_attachment" "marabot" { + user = aws_iam_user.marabot.name + policy_arn = aws_iam_policy.marabot.arn +} + +resource "aws_iam_access_key" "creds" { + user = aws_iam_user.marabot.name +} + +output "marabot_environment_file" { + value = <<EOT +AWS_ACCESS_KEY_ID=${aws_iam_access_key.creds.id} +AWS_SECRET_ACCESS_KEY=${nonsensitive(aws_iam_access_key.creds.secret)} +EOT +} |
