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

View File

@ -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
}

View File

@ -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)

View File

@ -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(`]`)