hmn/src/discord/integration.go

211 lines
5.5 KiB
Go

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() {
}