aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2023-06-17 19:59:20 -0400
committerXe Iaso <me@xeiaso.net>2023-06-17 19:59:20 -0400
commit276b1fdd41de087a6700a1578e71dd8ccf83fee6 (patch)
treed8e5f2e94333e392ae836d6dd6c77e78ee01f4ce /cmd
parent24f5fb57f3e36ff2f6150468c4cb10c4f5bb282c (diff)
downloadx-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.go105
-rw-r--r--cmd/marabot/irc.go41
-rw-r--r--cmd/marabot/main.go175
-rw-r--r--cmd/marabot/revolt.go107
-rw-r--r--cmd/marabot/schema.sql57
-rw-r--r--cmd/marabot/terraform/.gitignore1
-rw-r--r--cmd/marabot/terraform/.terraform.lock.hcl25
-rw-r--r--cmd/marabot/terraform/main.tf89
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
+}