Create snippets!
This commit is contained in:
parent
72ae938302
commit
ec64babdd6
|
@ -47,7 +47,7 @@ func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message) er
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
// save the message, maybe save its contents, and maybe make a snippet too
|
// save the message, maybe save its contents, and maybe make a snippet too
|
||||||
_, err = bot.saveMessageAndContents(ctx, tx, msg)
|
newMsg, err := bot.saveMessageAndContents(ctx, tx, msg)
|
||||||
if errors.Is(err, errNotEnoughInfo) {
|
if errors.Is(err, errNotEnoughInfo) {
|
||||||
logging.ExtractLogger(ctx).Warn().
|
logging.ExtractLogger(ctx).Warn().
|
||||||
Interface("msg", msg).
|
Interface("msg", msg).
|
||||||
|
@ -56,8 +56,8 @@ func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message) er
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if doSnippet, err := bot.allowedToCreateMessageSnippet(ctx, msg); doSnippet && err == nil {
|
if doSnippet, err := bot.allowedToCreateMessageSnippet(ctx, tx, newMsg.UserID); doSnippet && err == nil {
|
||||||
_, err := bot.createMessageSnippet(ctx, msg)
|
_, err := bot.createMessageSnippet(ctx, tx, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oops.New(err, "failed to create snippet in gateway")
|
return oops.New(err, "failed to create snippet in gateway")
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ func (bot *botInstance) saveMessageAndContents(
|
||||||
FROM handmade_discorduser
|
FROM handmade_discorduser
|
||||||
WHERE userid = $1
|
WHERE userid = $1
|
||||||
`,
|
`,
|
||||||
msg.Author.ID,
|
newMsg.UserID,
|
||||||
)
|
)
|
||||||
if errors.Is(err, db.ErrNoMatchingRows) {
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||||
return newMsg, nil
|
return newMsg, nil
|
||||||
|
@ -213,6 +213,7 @@ func (bot *botInstance) saveMessageAndContents(
|
||||||
// We have a linked Discord account, so save the message contents (regardless of
|
// We have a linked Discord account, so save the message contents (regardless of
|
||||||
// whether we create a snippet or not).
|
// whether we create a snippet or not).
|
||||||
|
|
||||||
|
if msg.OriginalHasFields("content") {
|
||||||
_, err = tx.Exec(ctx,
|
_, err = tx.Exec(ctx,
|
||||||
`
|
`
|
||||||
INSERT INTO handmade_discordmessagecontent (message_id, discord_id, last_content)
|
INSERT INTO handmade_discordmessagecontent (message_id, discord_id, last_content)
|
||||||
|
@ -221,10 +222,11 @@ func (bot *botInstance) saveMessageAndContents(
|
||||||
discord_id = EXCLUDED.discord_id,
|
discord_id = EXCLUDED.discord_id,
|
||||||
last_content = EXCLUDED.last_content
|
last_content = EXCLUDED.last_content
|
||||||
`,
|
`,
|
||||||
msg.ID,
|
newMsg.ID,
|
||||||
discordUser.ID,
|
discordUser.ID,
|
||||||
msg.Content, // TODO: Add a method that can fill in mentions and stuff (https://discord.com/developers/docs/reference#message-formatting)
|
msg.Content, // TODO: Add a method that can fill in mentions and stuff (https://discord.com/developers/docs/reference#message-formatting)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Save attachments
|
// Save attachments
|
||||||
for _, attachment := range msg.Attachments {
|
for _, attachment := range msg.Attachments {
|
||||||
|
@ -274,6 +276,10 @@ func downloadDiscordResource(ctx context.Context, url string) ([]byte, string, e
|
||||||
return content, res.Header.Get("Content-Type"), nil
|
return content, res.Header.Get("Content-Type"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Saves a Discord attachment as an HMN asset. Idempotent; will not create an attachment
|
||||||
|
that already exists
|
||||||
|
*/
|
||||||
func (bot *botInstance) saveAttachment(
|
func (bot *botInstance) saveAttachment(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx pgx.Tx,
|
tx pgx.Tx,
|
||||||
|
@ -281,7 +287,21 @@ func (bot *botInstance) saveAttachment(
|
||||||
hmnUserID int,
|
hmnUserID int,
|
||||||
discordMessageID string,
|
discordMessageID string,
|
||||||
) (*models.DiscordMessageAttachment, error) {
|
) (*models.DiscordMessageAttachment, error) {
|
||||||
// TODO: Return an existing attachment if it exists
|
iexisting, err := db.QueryOne(ctx, tx, models.DiscordMessageAttachment{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM handmade_discordmessageattachment
|
||||||
|
WHERE id = $1
|
||||||
|
`,
|
||||||
|
attachment.ID,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
return iexisting.(*models.DiscordMessageAttachment), nil
|
||||||
|
} else if errors.Is(err, db.ErrNoMatchingRows) {
|
||||||
|
// this is fine, just create it
|
||||||
|
} else {
|
||||||
|
return nil, oops.New(err, "failed to check for existing attachment")
|
||||||
|
}
|
||||||
|
|
||||||
width := 0
|
width := 0
|
||||||
height := 0
|
height := 0
|
||||||
|
@ -351,7 +371,8 @@ func (bot *botInstance) saveEmbed(
|
||||||
hmnUserID int,
|
hmnUserID int,
|
||||||
discordMessageID string,
|
discordMessageID string,
|
||||||
) (*models.DiscordMessageEmbed, error) {
|
) (*models.DiscordMessageEmbed, error) {
|
||||||
// TODO: Does this need to be idempotent
|
// TODO: Does this need to be idempotent? Embeds don't have IDs...
|
||||||
|
// Maybe Discord will never actually send us the same embed twice?
|
||||||
|
|
||||||
isOkImageType := func(contentType string) bool {
|
isOkImageType := func(contentType string) bool {
|
||||||
return strings.HasPrefix(contentType, "image/")
|
return strings.HasPrefix(contentType, "image/")
|
||||||
|
@ -446,7 +467,7 @@ func (bot *botInstance) saveEmbed(
|
||||||
return iDiscordEmbed.(*models.DiscordMessageEmbed), nil
|
return iDiscordEmbed.(*models.DiscordMessageEmbed), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *botInstance) allowedToCreateMessageSnippet(ctx context.Context, msg *Message) (bool, error) {
|
func (bot *botInstance) allowedToCreateMessageSnippet(ctx context.Context, tx pgx.Tx, discordUserId string) (bool, error) {
|
||||||
canSave, err := db.QueryBool(ctx, bot.dbConn,
|
canSave, err := db.QueryBool(ctx, bot.dbConn,
|
||||||
`
|
`
|
||||||
SELECT u.discord_save_showcase
|
SELECT u.discord_save_showcase
|
||||||
|
@ -456,7 +477,7 @@ func (bot *botInstance) allowedToCreateMessageSnippet(ctx context.Context, msg *
|
||||||
WHERE
|
WHERE
|
||||||
duser.userid = $1
|
duser.userid = $1
|
||||||
`,
|
`,
|
||||||
msg.Author.ID,
|
discordUserId,
|
||||||
)
|
)
|
||||||
if errors.Is(err, db.ErrNoMatchingRows) {
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -467,9 +488,140 @@ func (bot *botInstance) allowedToCreateMessageSnippet(ctx context.Context, msg *
|
||||||
return canSave, nil
|
return canSave, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *botInstance) createMessageSnippet(ctx context.Context, msg *Message) (*models.Snippet, error) {
|
func (bot *botInstance) createMessageSnippet(ctx context.Context, tx pgx.Tx, msg *Message) (*models.Snippet, error) {
|
||||||
// TODO: Actually do this
|
// Check for existing snippet, maybe return it
|
||||||
|
type existingSnippetResult struct {
|
||||||
|
Message models.DiscordMessage `db:"msg"`
|
||||||
|
MessageContent *models.DiscordMessageContent `db:"c"`
|
||||||
|
Snippet *models.Snippet `db:"snippet"`
|
||||||
|
DiscordUser *models.DiscordUser `db:"duser"`
|
||||||
|
}
|
||||||
|
iexisting, err := db.QueryOne(ctx, tx, existingSnippetResult{},
|
||||||
|
`
|
||||||
|
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
|
||||||
|
WHERE
|
||||||
|
msg.id = $1
|
||||||
|
`,
|
||||||
|
msg.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to check for existing snippet")
|
||||||
|
}
|
||||||
|
existing := iexisting.(*existingSnippetResult)
|
||||||
|
|
||||||
|
if existing.Snippet != nil {
|
||||||
|
// A snippet already exists
|
||||||
|
return existing.Snippet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing.Message.SnippetCreated {
|
||||||
|
// A snippet once existed but no longer does
|
||||||
|
// (we do not create another one in this case)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing.MessageContent == nil || existing.DiscordUser == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an asset ID or URL to make a snippet from
|
||||||
|
assetId, url, err := bot.getSnippetAssetOrUrl(ctx, tx, &existing.Message)
|
||||||
|
if assetId == nil && url == "" {
|
||||||
|
// Nothing to make a snippet from!
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentMarkdown := existing.MessageContent.LastContent
|
||||||
|
contentHTML := contentMarkdown // TODO: Actually parse Discord's Markdown
|
||||||
|
|
||||||
|
// TODO(db): Insert
|
||||||
|
isnippet, err := db.QueryOne(ctx, tx, models.Snippet{},
|
||||||
|
`
|
||||||
|
INSERT INTO handmade_snippet (url, "when", description, _description_html, asset_id, discord_message_id, owner_id)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING $columns
|
||||||
|
`,
|
||||||
|
nil,
|
||||||
|
existing.Message.SentAt,
|
||||||
|
contentMarkdown,
|
||||||
|
contentHTML,
|
||||||
|
assetId,
|
||||||
|
msg.ID,
|
||||||
|
existing.DiscordUser.HMNUserId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to create snippet from attachment")
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx,
|
||||||
|
`
|
||||||
|
UPDATE handmade_discordmessage
|
||||||
|
SET snippet_created = TRUE
|
||||||
|
WHERE id = $1
|
||||||
|
`,
|
||||||
|
msg.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to mark message as having snippet")
|
||||||
|
}
|
||||||
|
|
||||||
|
return isnippet.(*models.Snippet), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(ben): This is maybe redundant with the regexes we use for markdown. But
|
||||||
|
// do we actually want to reuse those, or should we keep them separate?
|
||||||
|
var RESnippetableUrl = regexp.MustCompile(`^https?://(youtu\.be|(www\.)?youtube\.com/watch)`)
|
||||||
|
|
||||||
|
func (bot *botInstance) getSnippetAssetOrUrl(ctx context.Context, tx pgx.Tx, msg *models.DiscordMessage) (*uuid.UUID, string, error) {
|
||||||
|
// Check attachments
|
||||||
|
itAttachments, err := db.Query(ctx, tx, models.DiscordMessageAttachment{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM handmade_discordmessageattachment
|
||||||
|
WHERE message_id = $1
|
||||||
|
`,
|
||||||
|
msg.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", oops.New(err, "failed to fetch message attachments")
|
||||||
|
}
|
||||||
|
attachments := itAttachments.ToSlice()
|
||||||
|
for _, iattachment := range attachments {
|
||||||
|
attachment := iattachment.(*models.DiscordMessageAttachment)
|
||||||
|
return &attachment.AssetID, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check embeds
|
||||||
|
itEmbeds, err := db.Query(ctx, tx, models.DiscordMessageEmbed{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM handmade_discordmessageembed
|
||||||
|
WHERE message_id = $1
|
||||||
|
`,
|
||||||
|
msg.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", oops.New(err, "failed to fetch discord embeds")
|
||||||
|
}
|
||||||
|
embeds := itEmbeds.ToSlice()
|
||||||
|
for _, iembed := range embeds {
|
||||||
|
embed := iembed.(*models.DiscordMessageEmbed)
|
||||||
|
if embed.VideoID != nil {
|
||||||
|
return embed.VideoID, "", nil
|
||||||
|
} else if embed.ImageID != nil {
|
||||||
|
return embed.ImageID, "", nil
|
||||||
|
} else if embed.URL != nil {
|
||||||
|
if RESnippetableUrl.MatchString(*embed.URL) {
|
||||||
|
return nil, *embed.URL, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func messageHasLinks(content string) bool {
|
func messageHasLinks(content string) bool {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerMigration(DiscordDefaults{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordDefaults struct{}
|
||||||
|
|
||||||
|
func (m DiscordDefaults) Version() types.MigrationVersion {
|
||||||
|
return types.MigrationVersion(time.Date(2021, 8, 23, 23, 5, 59, 0, time.UTC))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DiscordDefaults) Name() string {
|
||||||
|
return "DiscordDefaults"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DiscordDefaults) Description() string {
|
||||||
|
return "Add some default values to Discord fields"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DiscordDefaults) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, `
|
||||||
|
ALTER TABLE handmade_discordmessage
|
||||||
|
ALTER snippet_created SET DEFAULT FALSE;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to set message defaults")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, `
|
||||||
|
ALTER TABLE handmade_snippet
|
||||||
|
ALTER "when" SET DEFAULT NOW(),
|
||||||
|
ALTER edited_on_website SET DEFAULT FALSE;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to set snippet defaults")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DiscordDefaults) Down(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
panic("Implement me")
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ type Snippet struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
OwnerID int `db:"owner_id"`
|
OwnerID int `db:"owner_id"`
|
||||||
|
|
||||||
When time.Time `db:"when"`
|
When time.Time `db:"\"when\""`
|
||||||
|
|
||||||
Description string `db:"description"`
|
Description string `db:"description"`
|
||||||
DescriptionHtml string `db:"_description_html"`
|
DescriptionHtml string `db:"_description_html"`
|
||||||
|
|
Loading…
Reference in New Issue