Do the Discord integration with personal projects!

This commit is contained in:
Ben Visness 2021-12-08 21:13:58 -06:00
parent 37fcbb205c
commit 40cd19c5f0
4 changed files with 169 additions and 49 deletions

View File

@ -593,7 +593,7 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message)
} }
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID { if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
err := bot.processShowcaseMsg(ctx, msg, false) err := bot.processShowcaseMsg(ctx, msg)
if err != nil { if err != nil {
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process showcase message") logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process showcase message")
return nil return nil
@ -601,14 +601,14 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message)
return nil return nil
} }
if msg.ChannelID == config.Config.Discord.JamShowcaseChannelID { // if msg.ChannelID == config.Config.Discord.JamShowcaseChannelID {
err := bot.processShowcaseMsg(ctx, msg, true) // err := bot.processShowcaseMsg(ctx, msg)
if err != nil { // if err != nil {
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process jam showcase message") // logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process jam showcase message")
return nil // return nil
} // }
return nil // return nil
} // }
if msg.ChannelID == config.Config.Discord.LibraryChannelID { if msg.ChannelID == config.Config.Discord.LibraryChannelID {
err := bot.processLibraryMsg(ctx, msg) err := bot.processLibraryMsg(ctx, msg)

View File

@ -14,6 +14,7 @@ import (
"git.handmade.network/hmn/hmn/src/assets" "git.handmade.network/hmn/hmn/src/assets"
"git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db" "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/logging"
"git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops" "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") 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 // 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 { switch msg.Type {
case MessageTypeDefault, MessageTypeReply, MessageTypeApplicationCommand: case MessageTypeDefault, MessageTypeReply, MessageTypeApplicationCommand:
default: 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") return oops.New(err, "failed to create snippet in gateway")
} }
if isJam { u, err := FetchDiscordUser(ctx, bot.dbConn, newMsg.UserID)
tagId, err := db.QueryInt(ctx, tx, `SELECT id FROM tags WHERE text = 'wheeljam'`)
if err != nil { if err != nil {
return oops.New(err, "failed to fetch id of jam tag") 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 { if err != nil {
return oops.New(err, "failed to mark snippet as a jam snippet") 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 add tag to snippet")
} }
} }
} else if err != nil { } else if err != nil {
@ -519,29 +565,42 @@ func saveEmbed(
return iDiscordEmbed.(*models.DiscordMessageEmbed), nil return iDiscordEmbed.(*models.DiscordMessageEmbed), nil
} }
/* type DiscordUserAndStuff struct {
Checks settings and permissions to decide whether we are allowed to create DiscordUser models.DiscordUser `db:"duser"`
snippets for a user. HMNUser models.User `db:"u"`
*/ }
func AllowedToCreateMessageSnippet(ctx context.Context, tx db.ConnOrTx, discordUserId string) (bool, error) {
canSave, err := db.QueryBool(ctx, tx, 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 FROM
handmade_discorduser AS duser handmade_discorduser AS duser
JOIN auth_user AS u ON duser.hmn_user_id = u.id JOIN auth_user AS u ON duser.hmn_user_id = u.id
WHERE WHERE
duser.userid = $1 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) { if errors.Is(err, db.NotFound) {
return false, nil return false, nil
} else if err != nil { } else if err != nil {
return false, oops.New(err, "failed to check if we can save Discord message") 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 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
}

View File

@ -41,6 +41,33 @@ func FetchSnippets(
} }
defer tx.Rollback(ctx) 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 var qb db.QueryBuilder
qb.Add( qb.Add(
` `
@ -50,8 +77,6 @@ func FetchSnippets(
LEFT JOIN auth_user AS owner ON snippet.owner_id = owner.id 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_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 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 WHERE
TRUE TRUE
`, `,
@ -62,9 +87,6 @@ func FetchSnippets(
if len(q.OwnerIDs) > 0 { if len(q.OwnerIDs) > 0 {
qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs) 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 { if currentUser == nil {
qb.Add( qb.Add(
`AND owner.status = $? -- snippet owner is Approved`, `AND owner.status = $? -- snippet owner is Approved`,
@ -92,34 +114,59 @@ func FetchSnippets(
Owner *models.User `db:"owner"` Owner *models.User `db:"owner"`
Asset *models.Asset `db:"asset"` Asset *models.Asset `db:"asset"`
DiscordMessage *models.DiscordMessage `db:"discord_message"` 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 { if err != nil {
return nil, oops.New(err, "failed to fetch threads") return nil, oops.New(err, "failed to fetch threads")
} }
iresults := it.ToSlice() iresults := it.ToSlice()
result := make([]SnippetAndStuff, 0, len(iresults)) // allocate extra space because why not result := make([]SnippetAndStuff, len(iresults)) // allocate extra space because why not
currentSnippetId := -1 snippetIDs := make([]int, len(iresults))
for _, iresult := range iresults { for i, iresult := range iresults {
row := *iresult.(*resultRow) row := *iresult.(*resultRow)
if row.Snippet.ID != currentSnippetId { result[i] = SnippetAndStuff{
// we have moved onto a new snippet; make a new entry
result = append(result, SnippetAndStuff{
Snippet: row.Snippet, Snippet: row.Snippet,
Owner: row.Owner, Owner: row.Owner,
Asset: row.Asset, Asset: row.Asset,
DiscordMessage: row.DiscordMessage, DiscordMessage: row.DiscordMessage,
// no tags! tags next // no tags! tags next
}) }
snippetIDs[i] = row.Snippet.ID
} }
if row.Tag != nil { // Fetch tags
result[len(result)-1].Tags = append(result[len(result)-1].Tags, row.Tag) 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) err = tx.Commit(ctx)

View File

@ -370,7 +370,7 @@ func TimelineItemsToJSON(items []TimelineItem) string {
builder.WriteString(`",`) builder.WriteString(`",`)
builder.WriteString(`"tags":[`) builder.WriteString(`"tags":[`)
for _, tag := range item.Tags { for i, tag := range item.Tags {
builder.WriteString(`{`) builder.WriteString(`{`)
builder.WriteString(`"text":"`) builder.WriteString(`"text":"`)
@ -382,6 +382,9 @@ func TimelineItemsToJSON(items []TimelineItem) string {
builder.WriteString(`"`) builder.WriteString(`"`)
builder.WriteString(`}`) builder.WriteString(`}`)
if i < len(item.Tags)-1 {
builder.WriteString(`,`)
}
} }
builder.WriteString(`]`) builder.WriteString(`]`)