211 lines
5.5 KiB
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() {
|
|
|
|
}
|