diff --git a/src/discord/gateway.go b/src/discord/gateway.go index f2ef817..a35994a 100644 --- a/src/discord/gateway.go +++ b/src/discord/gateway.go @@ -593,7 +593,7 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message) } if msg.ChannelID == config.Config.Discord.ShowcaseChannelID { - err := bot.processShowcaseMsg(ctx, msg, false) + err := bot.processShowcaseMsg(ctx, msg) if err != nil { logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process showcase message") return nil @@ -601,14 +601,14 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message) return nil } - if msg.ChannelID == config.Config.Discord.JamShowcaseChannelID { - err := bot.processShowcaseMsg(ctx, msg, true) - if err != nil { - logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process jam showcase message") - return nil - } - return nil - } + // if msg.ChannelID == config.Config.Discord.JamShowcaseChannelID { + // err := bot.processShowcaseMsg(ctx, msg) + // if err != nil { + // logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process jam showcase message") + // return nil + // } + // return nil + // } if msg.ChannelID == config.Config.Discord.LibraryChannelID { err := bot.processLibraryMsg(ctx, msg) diff --git a/src/discord/showcase.go b/src/discord/showcase.go index 7ca9f3e..ad2dda0 100644 --- a/src/discord/showcase.go +++ b/src/discord/showcase.go @@ -14,6 +14,7 @@ import ( "git.handmade.network/hmn/hmn/src/assets" "git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/db" + "git.handmade.network/hmn/hmn/src/hmndata" "git.handmade.network/hmn/hmn/src/logging" "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/oops" @@ -26,7 +27,7 @@ var reDiscordMessageLink = regexp.MustCompile(`https?://.+?(\s|$)`) var errNotEnoughInfo = errors.New("Discord didn't send enough info in this event for us to do this") // TODO: Turn this ad-hoc isJam parameter into a tag or something -func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message, isJam bool) error { +func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message) error { switch msg.Type { case MessageTypeDefault, MessageTypeReply, MessageTypeApplicationCommand: default: @@ -63,15 +64,60 @@ func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message, is return oops.New(err, "failed to create snippet in gateway") } - if isJam { - tagId, err := db.QueryInt(ctx, tx, `SELECT id FROM tags WHERE text = 'wheeljam'`) - if err != nil { - return oops.New(err, "failed to fetch id of jam tag") - } + u, err := FetchDiscordUser(ctx, bot.dbConn, newMsg.UserID) + if err != nil { + return oops.New(err, "failed to look up HMN user information from Discord user") + // we shouldn't see a "not found" here because of the AllowedToBlahBlahBlah check. + } - _, err = tx.Exec(ctx, `INSERT INTO snippet_tags (snippet_id, tag_id) VALUES ($1, $2)`, snippet.ID, tagId) + projects, err := hmndata.FetchProjects(ctx, bot.dbConn, &u.HMNUser, hmndata.ProjectsQuery{ + OwnerIDs: []int{u.HMNUser.ID}, + }) + if err != nil { + return oops.New(err, "failed to look up user projects") + } + projectIDs := make([]int, len(projects)) + for i, p := range projects { + projectIDs[i] = p.Project.ID + } + + // Try to associate tags in the message with project tags in HMN. + // Match only tags for projects in which the current user is a collaborator. + messageTags := getDiscordTags(msg.Content) + type tagsRow struct { + Tag models.Tag `db:"tags"` + } + itUserTags, err := db.Query(ctx, tx, tagsRow{}, + ` + SELECT $columns + FROM + tags + JOIN handmade_project AS project ON project.tag = tags.id + JOIN handmade_user_projects AS user_project ON user_project.project_id = project.id + WHERE + project.id = ANY ($1) + `, + projectIDs, + ) + if err != nil { + return oops.New(err, "failed to fetch tags for user projects") + } + iUserTags := itUserTags.ToSlice() + + var tagIDs []int + for _, itag := range iUserTags { + tag := itag.(*tagsRow).Tag + for _, messageTag := range messageTags { + if tag.Text == messageTag { + tagIDs = append(tagIDs, tag.ID) + } + } + } + + for _, tagID := range tagIDs { + _, err = tx.Exec(ctx, `INSERT INTO snippet_tags (snippet_id, tag_id) VALUES ($1, $2)`, snippet.ID, tagID) if err != nil { - return oops.New(err, "failed to mark snippet as a jam snippet") + return oops.New(err, "failed to add tag to snippet") } } } else if err != nil { @@ -519,29 +565,42 @@ func saveEmbed( return iDiscordEmbed.(*models.DiscordMessageEmbed), nil } -/* -Checks settings and permissions to decide whether we are allowed to create -snippets for a user. -*/ -func AllowedToCreateMessageSnippet(ctx context.Context, tx db.ConnOrTx, discordUserId string) (bool, error) { - canSave, err := db.QueryBool(ctx, tx, +type DiscordUserAndStuff struct { + DiscordUser models.DiscordUser `db:"duser"` + HMNUser models.User `db:"u"` +} + +func FetchDiscordUser(ctx context.Context, dbConn db.ConnOrTx, discordUserID string) (*DiscordUserAndStuff, error) { + iuser, err := db.QueryOne(ctx, dbConn, DiscordUserAndStuff{}, ` - SELECT u.discord_save_showcase + SELECT $columns FROM handmade_discorduser AS duser JOIN auth_user AS u ON duser.hmn_user_id = u.id WHERE duser.userid = $1 `, - discordUserId, + discordUserID, ) + if err != nil { + return nil, err + } + return iuser.(*DiscordUserAndStuff), nil +} + +/* +Checks settings and permissions to decide whether we are allowed to create +snippets for a user. +*/ +func AllowedToCreateMessageSnippet(ctx context.Context, tx db.ConnOrTx, discordUserId string) (bool, error) { + u, err := FetchDiscordUser(ctx, tx, discordUserId) if errors.Is(err, db.NotFound) { return false, nil } else if err != nil { return false, oops.New(err, "failed to check if we can save Discord message") } - return canSave, nil + return u.HMNUser.DiscordSaveShowcase, nil } /* @@ -720,3 +779,14 @@ func messageHasLinks(content string) bool { return false } + +var REDiscordTag = regexp.MustCompile(`>([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)`) + +func getDiscordTags(content string) []string { + matches := REDiscordTag.FindAllStringSubmatch(content, -1) + result := make([]string, len(matches)) + for i, m := range matches { + result[i] = strings.ToLower(m[1]) + } + return result +} diff --git a/src/hmndata/snippet_helper.go b/src/hmndata/snippet_helper.go index 84e5c09..0fa1acb 100644 --- a/src/hmndata/snippet_helper.go +++ b/src/hmndata/snippet_helper.go @@ -41,6 +41,33 @@ func FetchSnippets( } defer tx.Rollback(ctx) + if len(q.Tags) > 0 { + // Get snippet IDs with this tag, then use that in the main query + type snippetIDRow struct { + SnippetID int `db:"snippet_id"` + } + itSnippetIDs, err := db.Query(ctx, tx, snippetIDRow{}, + ` + SELECT DISTINCT snippet_id + FROM + snippet_tags + JOIN tags ON snippet_tags.tag_id = tags.id + WHERE + tags.id = ANY ($1) + `, + q.Tags, + ) + if err != nil { + return nil, oops.New(err, "failed to get snippet IDs for tag") + } + iSnippetIDs := itSnippetIDs.ToSlice() + + q.IDs = make([]int, len(iSnippetIDs)) + for i := range iSnippetIDs { + q.IDs[i] = iSnippetIDs[i].(*snippetIDRow).SnippetID + } + } + var qb db.QueryBuilder qb.Add( ` @@ -50,8 +77,6 @@ func FetchSnippets( LEFT JOIN auth_user AS owner ON snippet.owner_id = owner.id LEFT JOIN handmade_asset AS asset ON snippet.asset_id = asset.id LEFT JOIN handmade_discordmessage AS discord_message ON snippet.discord_message_id = discord_message.id - LEFT JOIN snippet_tags ON snippet.id = snippet_tags.snippet_id - LEFT JOIN tags ON snippet_tags.tag_id = tags.id WHERE TRUE `, @@ -62,9 +87,6 @@ func FetchSnippets( if len(q.OwnerIDs) > 0 { qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs) } - if len(q.Tags) > 0 { - qb.Add(`AND snippet_tags.tag_id = ANY ($?)`, q.Tags) - } if currentUser == nil { qb.Add( `AND owner.status = $? -- snippet owner is Approved`, @@ -92,34 +114,59 @@ func FetchSnippets( Owner *models.User `db:"owner"` Asset *models.Asset `db:"asset"` DiscordMessage *models.DiscordMessage `db:"discord_message"` - Tag *models.Tag `db:"tags"` } - it, err := db.Query(ctx, dbConn, resultRow{}, qb.String(), qb.Args()...) + it, err := db.Query(ctx, tx, resultRow{}, qb.String(), qb.Args()...) if err != nil { return nil, oops.New(err, "failed to fetch threads") } iresults := it.ToSlice() - result := make([]SnippetAndStuff, 0, len(iresults)) // allocate extra space because why not - currentSnippetId := -1 - for _, iresult := range iresults { + result := make([]SnippetAndStuff, len(iresults)) // allocate extra space because why not + snippetIDs := make([]int, len(iresults)) + for i, iresult := range iresults { row := *iresult.(*resultRow) - if row.Snippet.ID != currentSnippetId { - // we have moved onto a new snippet; make a new entry - result = append(result, SnippetAndStuff{ - Snippet: row.Snippet, - Owner: row.Owner, - Asset: row.Asset, - DiscordMessage: row.DiscordMessage, - // no tags! tags next - }) + result[i] = SnippetAndStuff{ + Snippet: row.Snippet, + Owner: row.Owner, + Asset: row.Asset, + DiscordMessage: row.DiscordMessage, + // no tags! tags next } + snippetIDs[i] = row.Snippet.ID + } - if row.Tag != nil { - result[len(result)-1].Tags = append(result[len(result)-1].Tags, row.Tag) - } + // Fetch tags + type snippetTagRow struct { + SnippetID int `db:"snippet_tags.snippet_id"` + Tag *models.Tag `db:"tags"` + } + itSnippetTags, err := db.Query(ctx, tx, snippetTagRow{}, + ` + SELECT $columns + FROM + snippet_tags + JOIN tags ON snippet_tags.tag_id = tags.id + WHERE + snippet_tags.snippet_id = ANY($1) + `, + snippetIDs, + ) + if err != nil { + return nil, oops.New(err, "failed to fetch tags for snippets") + } + iSnippetTags := itSnippetTags.ToSlice() + + // associate tags with snippets + resultBySnippetId := make(map[int]*SnippetAndStuff) + for i := range result { + resultBySnippetId[result[i].Snippet.ID] = &result[i] + } + for _, iSnippetTag := range iSnippetTags { + snippetTag := iSnippetTag.(*snippetTagRow) + item := resultBySnippetId[snippetTag.SnippetID] + item.Tags = append(item.Tags, snippetTag.Tag) } err = tx.Commit(ctx) diff --git a/src/templates/mapping.go b/src/templates/mapping.go index 7b154ba..f6f178c 100644 --- a/src/templates/mapping.go +++ b/src/templates/mapping.go @@ -370,7 +370,7 @@ func TimelineItemsToJSON(items []TimelineItem) string { builder.WriteString(`",`) builder.WriteString(`"tags":[`) - for _, tag := range item.Tags { + for i, tag := range item.Tags { builder.WriteString(`{`) builder.WriteString(`"text":"`) @@ -382,6 +382,9 @@ func TimelineItemsToJSON(items []TimelineItem) string { builder.WriteString(`"`) builder.WriteString(`}`) + if i < len(item.Tags)-1 { + builder.WriteString(`,`) + } } builder.WriteString(`]`)