package discord import ( "context" "errors" "net/url" "strings" "git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/oops" "github.com/jackc/pgx/v4" ) // // READ THE README! // (./README.md) // type MessageCreatedOrUpdatedFlags int const ( // If set, snippet creation will generally be enabled. We will attempt to // make snippets for new messages shared in #project-showcase or whatever. // // If not set, snippet updates and deletes will still occur where // appropriate. But new snippets will never be created. CreateSnippetsOnNewMessages MessageCreatedOrUpdatedFlags = 1 << iota // Create a snippet even if a message is not in the usual channels. This // will NOT, in fact, create a snippet if the message doesn't meet the // conditions - e.g. the message has media, and the user has a linked // Discord account. // // Still requires CreateSnippetsOnNewMessages to be set as well. ForceSnippet ) // Call whenever you have a new or updated Discord message, in any channel. // For example, when: // - You receive a message create/update event from the gateway // - You see a new message in a history backfill // - An admin command wants to make a snippet from some arbitrary message // // This implements the "on new/updated message" flow in the readme. func MessageCreatedOrUpdated( ctx context.Context, dbConn db.ConnOrTx, msg *Message, flags MessageCreatedOrUpdatedFlags, ) error { return db.DoWithTransaction(ctx, dbConn, func(tx pgx.Tx) error { if msg.OriginalHasFields("author") && msg.Author.ID == config.Config.Discord.BotUserID { // Don't process messages from the bot return nil } didDelete, err := maybeDeleteShowcaseMsg(ctx, dbConn, msg) if err != nil { return err } else if didDelete { return nil } didDelete, err = maybeDeleteLibraryMsg(ctx, dbConn, msg) if err != nil { return err } else if didDelete { return nil } weCareAboutThisMessagesContents := false || flags&ForceSnippet != 0 || msg.ChannelID == config.Config.Discord.ShowcaseChannelID || weAlreadyHaveMessageContent(ctx, dbConn, msg) if weCareAboutThisMessagesContents { _, err := PersistMessage(ctx, dbConn, msg) if err != nil { return oops.New(err, "failed to persist new/updated Discord message") } } // // It's snippet time. // { // Fetch the data we need to evaluate what to do from here. type savedMessageAndSnippet struct { Message models.DiscordMessage `db:"msg"` MessageContent *models.DiscordMessageContent `db:"c"` Snippet *models.Snippet `db:"snippet"` DiscordUser *models.DiscordUser `db:"duser"` HMNUser *models.User `db:"u"` } isaved, err := db.QueryOne(ctx, tx, savedMessageAndSnippet{}, ` SELECT $columns FROM handmade_discordmessage AS msg LEFT JOIN handmade_discordmessagecontent AS c ON c.message_id = msg.id LEFT JOIN handmade_snippet AS snippet ON snippet.discord_message_id = msg.id LEFT JOIN handmade_discorduser AS duser ON msg.user_id = duser.userid LEFT JOIN auth_user AS u ON duser.hmn_user_id = u.id WHERE msg.id = $1 `, msg.ID, ) if errors.Is(err, db.NotFound) { goto nosnippet } else if err != nil { return oops.New(err, "failed to fetch snippet data and stuff") } saved := isaved.(*savedMessageAndSnippet) if saved.DiscordUser == nil { goto nosnippet } snippetableAsset, snippetableUrl, err := getSnippetAssetOrUrl(ctx, tx, msg.ID) if err != nil { return err } messageIsSnippetable := snippetableAsset != nil || snippetableUrl != "" if messageIsSnippetable { createSnippet, updateSnippet := false, false if saved.Snippet == nil { // No snippet exists. Maybe create one? wantSnippetBecauseShowcase := true && msg.ChannelID == config.Config.Discord.ShowcaseChannelID && saved.HMNUser.DiscordSaveShowcase wantNewSnippet := false || wantSnippetBecauseShowcase || flags&ForceSnippet != 0 createSnippet = true && flags&CreateSnippetsOnNewMessages != 0 && wantNewSnippet && saved.MessageContent != nil && saved.Message.SnippetCreated == false } else { // Update the snippet updateSnippet = true } if createSnippet { // TODO } else if updateSnippet { // TODO } } else { // Delete any existing snippets (don't restrict this by channel!) // Message edits can happen anywhere, even in channels where we don't // delete messages like we do in showcase. // TODO } } nosnippet: return nil }) } // Call whenever a Discord message is deleted, in any channel. func MessageDeleted(ctx context.Context, dbConn db.ConnOrTx, msg *Message) { } func messageHasLinks(content string) bool { links := reDiscordMessageLink.FindAllString(content, -1) for _, link := range links { _, err := url.Parse(strings.TrimSpace(link)) if err == nil { return true } } return false } func weAlreadyHaveMessageContent( ctx context.Context, dbConn db.ConnOrTx, msg *Message, ) bool { hasContent, err := db.QueryBool(ctx, dbConn, ` SELECT COUNT(*) > 0 FROM handmade_discordmessagecontent WHERE message_id = $1 `, msg.ID, ) if err != nil { panic(err) } return hasContent } // Yields HTML and tags for a snippet, but doesn't actually create the snippet. func messageToSnippetData() { }