From 189033cbff79ed5c4fead64d98a7af403cc2f06d Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Sun, 12 Jul 2026 12:03:36 -0400 Subject: [PATCH] Add ability to view message context --- server/database.go | 44 +++++++++++++++++++- server/report.go | 100 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/server/database.go b/server/database.go index 82bf46c..4752be5 100644 --- a/server/database.go +++ b/server/database.go @@ -1667,7 +1667,7 @@ func getReportersForPlayer(targetUuid, msgId string) (result map[string]string, JOIN playerGameData pgd ON pgd.uuid = pr.uuid WHERE pr.targetUuid = ? AND pr.msgId = ? AND NOT actionTaken`, targetUuid, msgIdLink) if err != nil { - return result, err + return } result = make(map[string]string) @@ -1686,6 +1686,48 @@ func getReportersForPlayer(targetUuid, msgId string) (result map[string]string, return result, nil } +func getChatMessageContext(msgId string) (result []ChatContext, err error) { + if msgId == "" { + err = errors.New("msgId must be nonempty") + return + } + + var ( + timestamp time.Time + game string + partyId any // string | nil + ) + err = db.QueryRow("SELECT timestamp, game, partyId FROM chatMessage WHERE msgId = ?", msgId).Scan(×tamp, &game, &partyId) + if err != nil { + return + } + + rows, err := db.Query(` + SELECT m.timestamp, pgd.name, m.uuid, m.msgId, m.contents, m.partyId, p.name AS partyName + FROM chatMessages m + JOIN playerGameData pgd ON pgd.uuid = m.uuid AND pgd.game = m.game + LEFT JOIN parties p ON p.id = m.partyId AND p.game = m.game + WHERE m.game = ? AND m.partyId <=> ? AND m.timestamp BETWEEN DATE_SUB(?, INTERVAL 1 MINUTE) AND ? + ORDER BY m.timestamp + `, game, partyId, timestamp, timestamp) + if err != nil { + return + } + + defer rows.Close() + for rows.Next() { + var ctx ChatContext + err = rows.Scan(&ctx.timestamp, &ctx.name, &ctx.uuid, &ctx.msgId, &ctx.contents, &ctx.partyId, &ctx.partyName) + if err != nil { + return nil, err + } + + result = append(result, ctx) + } + + return +} + func doCleanupQueries() error { // Remove player sessions that have expired _, err := db.Exec("DELETE FROM playerSessions WHERE expiration < NOW()") diff --git a/server/report.go b/server/report.go index a841ffb..4a2404c 100644 --- a/server/report.go +++ b/server/report.go @@ -56,6 +56,12 @@ type ModAction struct { action int } +type ChatContext struct { + timestamp time.Time + name, uuid, msgId, contents string + partyId, partyName sql.NullString +} + type oneshotJob struct { timer *time.Timer expiry time.Time @@ -180,6 +186,8 @@ func initModBot() { } delete(reportLog[uuid], ynoMsgId) markAsResolved(uuid) + case "ctx": + provideChatMessageContext(&resp, ynoMsgId) case "cmd": if len(data.Values) != 1 { return @@ -191,9 +199,9 @@ func initModBot() { log.Printf("getReportersForPlayer: %s", err) return } - reportsContent := "" + var reportsContent strings.Builder for reporter, reason := range reports { - reportsContent += fmt.Sprintf("%s: `%s` \n", reporter, getReadableReportReason(reason)) + fmt.Fprintf(&reportsContent, "%s: `%s` \n", reporter, getReadableReportReason(reason)) } resp.Type = discordgo.InteractionResponseChannelMessageWithSource resp.Data = &discordgo.InteractionResponseData{ @@ -201,7 +209,7 @@ func initModBot() { Embeds: []*discordgo.MessageEmbed{ { Title: fmt.Sprintf("Reporters for `msgid=%s`", ynoMsgId), - Description: reportsContent, + Description: reportsContent.String(), }, }, } @@ -265,7 +273,6 @@ func initModBot() { } if err = registerBotCommands(); err != nil { - log.Printf("bot/registerBotCommands: %s", err) return } @@ -308,11 +315,7 @@ func botHandleModalResponse(resp *discordgo.InteractionResponse, data discordgo. expiryRaw, reason := parseTempBanReportComponents(data.Components) expiryDuration, err := time.ParseDuration(expiryRaw) if err != nil { - resp.Type = discordgo.InteractionResponseChannelMessageWithSource - resp.Data = &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: fmt.Sprintf("`%s` is not a valid duration string", expiryRaw), - } + setResponse(resp, fmt.Sprintf("`%s` is not a valid duration string", expiryRaw)) return } @@ -386,11 +389,19 @@ WHERE pgd.name = ? OR pgd.uuid = ?`, playerid, playerid) onlineGames = append(onlineGames, game) } } - msg := fmt.Sprintf(`##### Player Info + msg := fmt.Sprintf(`### Player Info name=%s uuid=%s banned=%t muted=%t online in: %s`, name, uuid, banned, muted, strings.Join(onlineGames, ", ")) setResponse(resp, msg) + case "msgContext": + if len(args) != 1 { + setResponse(resp, "Usage: /msgContext ") + return + } + + ynoMsgId := args[0].StringValue() + provideChatMessageContext(resp, ynoMsgId) default: setResponse(resp, "Unknown command") } @@ -417,6 +428,29 @@ func registerBotCommands() (err error) { }, }, ) + if err != nil { + log.Printf("registerBotCommands/pinfo: %s", err) + } + + _, err = bot.ApplicationCommandCreate( + bot.State.User.ID, + config.moderation.guildId, + &discordgo.ApplicationCommand{ + Name: "msgContext", + Description: "Show 1-minute context of message", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "msgid", + Description: "message id in database", + Required: true, + }, + }, + }, + ) + if err != nil { + log.Printf("registerBotCommands/msgContext: %s", err) + } return } @@ -500,9 +534,9 @@ func formatReportLog(obj any, targetUuid, ynoMsgId, originalMsg, game string, re if originalMsg != "" { originalMsg = fmt.Sprintf("> *%s*", originalMsg) } - reasonsString := "" + var reasonsString strings.Builder for reason, count := range reasons { - reasonsString += fmt.Sprintf("- `%s`: %d\n", getReadableReportReason(reason), count) + fmt.Fprintf(&reasonsString, "- `%s`: %d\n", getReadableReportReason(reason), count) } verifiedString := "false" @@ -521,7 +555,7 @@ func formatReportLog(obj any, targetUuid, ynoMsgId, originalMsg, game string, re Fields: []*discordgo.MessageEmbedField{ { Name: "Reasons", - Value: reasonsString, + Value: reasonsString.String(), }, { Name: "Verified", @@ -538,6 +572,7 @@ func formatReportLog(obj any, targetUuid, ynoMsgId, originalMsg, game string, re components := []discordgo.MessageComponent{ discordgo.ActionsRow{ + // up to 5 only, everything else goes in options Components: []discordgo.MessageComponent{ discordgo.Button{ Label: "Ban", @@ -554,6 +589,11 @@ func formatReportLog(obj any, targetUuid, ynoMsgId, originalMsg, game string, re CustomID: "ack:" + targetUuid, Style: discordgo.SecondaryButton, }, + discordgo.Button{ + Label: "Context", + CustomID: "ctx:" + targetUuid, + Style: discordgo.SecondaryButton, + }, }, }, } @@ -756,6 +796,40 @@ VALUES return msgId, originalMsg, err } +func provideChatMessageContext(resp *discordgo.InteractionResponse, ynoMsgId string) { + context, err := getChatMessageContext(ynoMsgId) + if err != nil { + setResponse(resp, fmt.Sprintf("Error getting message context:\n%s", err)) + return + } + + if len(context) == 0 { + setResponse(resp, "No context available for this message") + return + } + + var responseContent strings.Builder + fmt.Fprint(&responseContent, "```\n") + for _, line := range context { + if line.partyName.Valid { + fmt.Fprintf(&responseContent, "(%s) ", line.partyName.String) + } + fmt.Fprintf(&responseContent, "[%s msgid:%s %s] %s\n", line.timestamp.Format(time.DateTime), line.msgId, line.name, line.contents) + } + fmt.Fprint(&responseContent, "```") + + resp.Type = discordgo.InteractionResponseChannelMessageWithSource + resp.Data = &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Embeds: []*discordgo.MessageEmbed{ + { + Title: fmt.Sprintf("Context for `msgid=%s`", ynoMsgId), + Description: responseContent.String(), + }, + }, + } +} + func markAsResolved(targetUuid string) { _, err := db.Exec(`UPDATE playerReports SET actionTaken = 1 WHERE targetUuid = ?`, targetUuid) if err != nil {