Link to HMN profiles from fishbowls where available

This commit is contained in:
Ben Visness 2022-06-11 17:51:12 -05:00
parent df4ff592b4
commit 7b5bf65c7b
4 changed files with 70 additions and 6 deletions

View File

@ -12,8 +12,9 @@ import (
type UsersQuery struct {
// Ignored when using FetchUser
UserIDs []int // if empty, all users
Usernames []string // if empty, all users
UserIDs []int // if empty, all users
Usernames []string // if empty, all users
DiscordUserIDs []string // if empty, no Discord filtering
// Flags to modify behavior
AnyStatus bool // Bypasses shadowban system
@ -44,8 +45,9 @@ func FetchUsers(
}
type userRow struct {
User models.User `db:"hmn_user"`
AvatarAsset *models.Asset `db:"avatar"`
User models.User `db:"hmn_user"`
AvatarAsset *models.Asset `db:"avatar"`
DiscordUser *models.DiscordUser `db:"discord_user"`
}
var qb db.QueryBuilder
@ -54,6 +56,7 @@ func FetchUsers(
FROM
hmn_user
LEFT JOIN asset AS avatar ON avatar.id = hmn_user.avatar_asset_id
LEFT JOIN discord_user ON discord_user.hmn_user_id = hmn_user.id
WHERE
TRUE
`)
@ -63,6 +66,9 @@ func FetchUsers(
if len(q.Usernames) > 0 {
qb.Add(`AND LOWER(hmn_user.username) = ANY($?)`, q.Usernames)
}
if len(q.DiscordUserIDs) > 0 {
qb.Add(`AND discord_user.userid = ANY($?)`, q.DiscordUserIDs)
}
if !q.AnyStatus {
if currentUser == nil {
qb.Add(`AND hmn_user.status = $?`, models.UserStatusApproved)
@ -89,6 +95,7 @@ func FetchUsers(
for i, row := range userRows {
user := row.User
user.AvatarAsset = row.AvatarAsset
user.DiscordUser = row.DiscordUser
result[i] = &user
}

View File

@ -50,6 +50,7 @@ type User struct {
// Non-db fields, to be filled in by fetch helpers
AvatarAsset *Asset
DiscordUser *DiscordUser
}
func (u *User) BestName() string {

View File

@ -65,6 +65,10 @@
.fishbowl-banner a {
color: {{ eq .Theme "dark" | ternary "#9ad0ff" "#1f4f99" }};
}
.fishbowl .chatlog__author a {
color: inherit;
}
</style>
{{ end }}

View File

@ -1,15 +1,20 @@
package website
import (
"fmt"
"html/template"
"io"
"io/fs"
"net/http"
"regexp"
"sort"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmndata"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
@ -157,6 +162,11 @@ func Fishbowl(c *RequestContext) ResponseData {
}
contentsFile := utils.Must1(fishbowlFS.Open(info.ContentsPath))
contents := string(utils.Must1(io.ReadAll(contentsFile)))
contents, err := linkifyDiscordContent(c, c.Conn, contents)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to linkify fishbowl content"))
}
tmpl := FishbowlData{
BaseData: getBaseData(c, info.Title, []templates.Breadcrumb{
@ -165,7 +175,7 @@ func Fishbowl(c *RequestContext) ResponseData {
}),
Slug: slug,
Info: info,
Contents: template.HTML(utils.Must1(io.ReadAll(contentsFile))),
Contents: template.HTML(contents),
}
tmpl.BaseData.OpenGraphItems = append(tmpl.BaseData.OpenGraphItems, templates.OpenGraphItem{
Property: "og:description",
@ -173,7 +183,7 @@ func Fishbowl(c *RequestContext) ResponseData {
})
var res ResponseData
err := res.WriteTemplate("fishbowl.html", tmpl, c.Perf)
err = res.WriteTemplate("fishbowl.html", tmpl, c.Perf)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render fishbowl index page"))
}
@ -186,3 +196,45 @@ func FishbowlFiles(c *RequestContext) ResponseData {
AddCORSHeaders(c, &res)
return res
}
var reFishbowlDiscordUserId = regexp.MustCompile(`data-user-id="(\d+)"`)
var reFishbowlDiscordAuthorHeader = regexp.MustCompile(`(?s:(<div class="chatlog__message">.*?)(<img class="chatlog__avatar".*?>)(.*?<span class="chatlog__author".*?data-user-id="(\d+)".*?>)(.*?)(</span>))`)
func linkifyDiscordContent(c *RequestContext, dbConn db.ConnOrTx, content string) (string, error) {
discordUserIdSet := make(map[string]struct{})
userIdMatches := reFishbowlDiscordUserId.FindAllStringSubmatch(content, -1)
for _, m := range userIdMatches {
discordUserIdSet[m[1]] = struct{}{}
}
discordUserIds := make([]string, 0, len(discordUserIdSet))
for id := range discordUserIdSet {
discordUserIds = append(discordUserIds, id)
}
hmnUsers, err := hmndata.FetchUsers(c.Context(), dbConn, c.CurrentUser, hmndata.UsersQuery{
DiscordUserIDs: discordUserIds,
})
if err != nil {
return "", err
}
return reFishbowlDiscordAuthorHeader.ReplaceAllStringFunc(content, func(s string) string {
m := reFishbowlDiscordAuthorHeader.FindStringSubmatch(s)
discordUserID := m[4]
var matchingUser *models.User
for _, u := range hmnUsers {
if u.DiscordUser.UserID == discordUserID {
matchingUser = u
break
}
}
if matchingUser == nil {
return s
} else {
link := fmt.Sprintf(`<a href="%s" target="_blank">`, hmnurl.BuildUserProfile(matchingUser.Username))
return m[1] + link + m[2] + "</a>" + m[3] + link + m[5] + "</a>" + m[6]
}
}), nil
}