Link to HMN profiles from fishbowls where available
This commit is contained in:
parent
df4ff592b4
commit
7b5bf65c7b
|
@ -12,8 +12,9 @@ import (
|
||||||
|
|
||||||
type UsersQuery struct {
|
type UsersQuery struct {
|
||||||
// Ignored when using FetchUser
|
// Ignored when using FetchUser
|
||||||
UserIDs []int // if empty, all users
|
UserIDs []int // if empty, all users
|
||||||
Usernames []string // if empty, all users
|
Usernames []string // if empty, all users
|
||||||
|
DiscordUserIDs []string // if empty, no Discord filtering
|
||||||
|
|
||||||
// Flags to modify behavior
|
// Flags to modify behavior
|
||||||
AnyStatus bool // Bypasses shadowban system
|
AnyStatus bool // Bypasses shadowban system
|
||||||
|
@ -44,8 +45,9 @@ func FetchUsers(
|
||||||
}
|
}
|
||||||
|
|
||||||
type userRow struct {
|
type userRow struct {
|
||||||
User models.User `db:"hmn_user"`
|
User models.User `db:"hmn_user"`
|
||||||
AvatarAsset *models.Asset `db:"avatar"`
|
AvatarAsset *models.Asset `db:"avatar"`
|
||||||
|
DiscordUser *models.DiscordUser `db:"discord_user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var qb db.QueryBuilder
|
var qb db.QueryBuilder
|
||||||
|
@ -54,6 +56,7 @@ func FetchUsers(
|
||||||
FROM
|
FROM
|
||||||
hmn_user
|
hmn_user
|
||||||
LEFT JOIN asset AS avatar ON avatar.id = hmn_user.avatar_asset_id
|
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
|
WHERE
|
||||||
TRUE
|
TRUE
|
||||||
`)
|
`)
|
||||||
|
@ -63,6 +66,9 @@ func FetchUsers(
|
||||||
if len(q.Usernames) > 0 {
|
if len(q.Usernames) > 0 {
|
||||||
qb.Add(`AND LOWER(hmn_user.username) = ANY($?)`, q.Usernames)
|
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 !q.AnyStatus {
|
||||||
if currentUser == nil {
|
if currentUser == nil {
|
||||||
qb.Add(`AND hmn_user.status = $?`, models.UserStatusApproved)
|
qb.Add(`AND hmn_user.status = $?`, models.UserStatusApproved)
|
||||||
|
@ -89,6 +95,7 @@ func FetchUsers(
|
||||||
for i, row := range userRows {
|
for i, row := range userRows {
|
||||||
user := row.User
|
user := row.User
|
||||||
user.AvatarAsset = row.AvatarAsset
|
user.AvatarAsset = row.AvatarAsset
|
||||||
|
user.DiscordUser = row.DiscordUser
|
||||||
result[i] = &user
|
result[i] = &user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ type User struct {
|
||||||
|
|
||||||
// Non-db fields, to be filled in by fetch helpers
|
// Non-db fields, to be filled in by fetch helpers
|
||||||
AvatarAsset *Asset
|
AvatarAsset *Asset
|
||||||
|
DiscordUser *DiscordUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) BestName() string {
|
func (u *User) BestName() string {
|
||||||
|
|
|
@ -65,6 +65,10 @@
|
||||||
.fishbowl-banner a {
|
.fishbowl-banner a {
|
||||||
color: {{ eq .Theme "dark" | ternary "#9ad0ff" "#1f4f99" }};
|
color: {{ eq .Theme "dark" | ternary "#9ad0ff" "#1f4f99" }};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fishbowl .chatlog__author a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/hmnurl"
|
||||||
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
"git.handmade.network/hmn/hmn/src/utils"
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
|
@ -157,6 +162,11 @@ func Fishbowl(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
contentsFile := utils.Must1(fishbowlFS.Open(info.ContentsPath))
|
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{
|
tmpl := FishbowlData{
|
||||||
BaseData: getBaseData(c, info.Title, []templates.Breadcrumb{
|
BaseData: getBaseData(c, info.Title, []templates.Breadcrumb{
|
||||||
|
@ -165,7 +175,7 @@ func Fishbowl(c *RequestContext) ResponseData {
|
||||||
}),
|
}),
|
||||||
Slug: slug,
|
Slug: slug,
|
||||||
Info: info,
|
Info: info,
|
||||||
Contents: template.HTML(utils.Must1(io.ReadAll(contentsFile))),
|
Contents: template.HTML(contents),
|
||||||
}
|
}
|
||||||
tmpl.BaseData.OpenGraphItems = append(tmpl.BaseData.OpenGraphItems, templates.OpenGraphItem{
|
tmpl.BaseData.OpenGraphItems = append(tmpl.BaseData.OpenGraphItems, templates.OpenGraphItem{
|
||||||
Property: "og:description",
|
Property: "og:description",
|
||||||
|
@ -173,7 +183,7 @@ func Fishbowl(c *RequestContext) ResponseData {
|
||||||
})
|
})
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
err := res.WriteTemplate("fishbowl.html", tmpl, c.Perf)
|
err = res.WriteTemplate("fishbowl.html", tmpl, c.Perf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render fishbowl index page"))
|
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)
|
AddCORSHeaders(c, &res)
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue