unbot/features/starboard.go
2026-06-02 04:22:00 +02:00

253 lines
6.6 KiB
Go

package features
import (
"bytes"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/events"
"github.com/disgoorg/disgo/rest"
"github.com/disgoorg/disgo/webhook"
"github.com/disgoorg/snowflake/v2"
"github.com/elisiei/unbot/db"
)
type Starboard struct {
db *db.DB
config StarboardConfig
webhook *webhook.Client
}
type StarboardConfig struct {
Enabled bool
WebhookURL string
ChannelID uint64
Threshold int
}
func NewStarboard(database *db.DB, webhookURL string, channelID uint64, threshold int, enabled bool) (*Starboard, error) {
var wh *webhook.Client
if webhookURL != "" {
var err error
wh, err = webhook.NewWithURL(webhookURL)
if err != nil {
return nil, fmt.Errorf("failed to create webhook: %w", err)
}
}
return &Starboard{
db: database,
webhook: wh,
config: StarboardConfig{
Enabled: enabled,
WebhookURL: webhookURL,
ChannelID: channelID,
Threshold: threshold,
},
}, nil
}
func (s *Starboard) HandleReactionAdd(e *events.GuildMessageReactionAdd) {
if !s.config.Enabled || s.webhook == nil {
return
}
if e.ChannelID == snowflake.ID(s.config.ChannelID) {
return
}
originalMsgID := e.MessageID.String()
msg, err := e.Client().Rest.GetMessage(e.ChannelID, e.MessageID)
if err != nil {
slog.Error("failed to get message for starboard", slog.Any("err", err))
return
}
totalReactions := 0
for _, r := range msg.Reactions {
totalReactions += r.Count
}
starboardMsgIDStr, err := s.db.GetStarboardMessageID(originalMsgID)
if err != nil {
slog.Error("failed to check starboard entry", slog.Any("err", err))
return
}
if starboardMsgIDStr != "" {
if totalReactions >= s.config.Threshold {
s.updateStarboardEntry(starboardMsgIDStr, msg, e.ChannelID, e.GuildID, e.MessageID)
} else {
s.removeStarboardEntry(starboardMsgIDStr, originalMsgID)
}
return
}
if totalReactions < s.config.Threshold {
return
}
content, embeds, files := s.buildStarboardMessage(msg, e.ChannelID, e.GuildID, e.MessageID)
msgCreate := discord.WebhookMessageCreate{
Content: content,
Embeds: embeds,
Files: files,
Username: msg.Author.EffectiveName(),
AvatarURL: msg.Author.EffectiveAvatarURL(),
}
webhookMsg, err := s.webhook.CreateMessage(msgCreate, rest.CreateWebhookMessageParams{Wait: true})
if err != nil {
slog.Error("failed to send starboard webhook", slog.Any("err", err))
return
}
if webhookMsg == nil {
slog.Error("starboard webhook returned nil message")
return
}
if err := s.db.AddStarboardEntry(originalMsgID, webhookMsg.ID.String()); err != nil {
slog.Error("failed to save starboard entry", slog.Any("err", err))
}
}
func (s *Starboard) HandleReactionRemove(e *events.GuildMessageReactionRemove) {
if !s.config.Enabled || s.webhook == nil {
return
}
if e.ChannelID == snowflake.ID(s.config.ChannelID) {
return
}
originalMsgID := e.MessageID.String()
starboardMsgIDStr, err := s.db.GetStarboardMessageID(originalMsgID)
if err != nil {
slog.Error("failed to check starboard entry", slog.Any("err", err))
return
}
if starboardMsgIDStr == "" {
return
}
msg, err := e.Client().Rest.GetMessage(e.ChannelID, e.MessageID)
if err != nil {
slog.Warn("original message gone, removing starboard entry", slog.Any("err", err))
s.removeStarboardEntry(starboardMsgIDStr, originalMsgID)
return
}
totalReactions := 0
for _, r := range msg.Reactions {
totalReactions += r.Count
}
if totalReactions >= s.config.Threshold {
s.updateStarboardEntry(starboardMsgIDStr, msg, e.ChannelID, e.GuildID, e.MessageID)
} else {
s.removeStarboardEntry(starboardMsgIDStr, originalMsgID)
}
}
func (s *Starboard) buildStarboardMessage(msg *discord.Message, channelID, guildID, messageID snowflake.ID) (content string, embeds []discord.Embed, files []*discord.File) {
jumpURL := fmt.Sprintf("https://discord.com/channels/%s/%s/%s", guildID, channelID, messageID)
content = jumpURL
embed := discord.Embed{
Author: &discord.EmbedAuthor{
Name: msg.Author.EffectiveName(),
IconURL: msg.Author.EffectiveAvatarURL(),
},
}
if msg.Content != "" {
desc := msg.Content
if len(desc) > 4096 {
desc = desc[:4093] + "..."
}
embed.Description = desc
}
for _, a := range msg.Attachments {
resp, err := http.Get(a.URL)
if err != nil {
slog.Warn("failed to download attachment for starboard", slog.Any("err", err), slog.String("url", a.URL))
continue
}
data, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
slog.Warn("failed to read attachment for starboard", slog.Any("err", err), slog.String("url", a.URL))
continue
}
files = append(files, discord.NewFile(a.Filename, "", bytes.NewReader(data)))
}
if len(msg.Reactions) > 0 {
var parts []string
for _, r := range msg.Reactions {
var e string
if r.Emoji.ID == 0 {
e = r.Emoji.Name
} else if r.Emoji.Animated {
e = fmt.Sprintf("<a:%s:%d>", r.Emoji.Name, r.Emoji.ID)
} else {
e = fmt.Sprintf("<:%s:%d>", r.Emoji.Name, r.Emoji.ID)
}
parts = append(parts, fmt.Sprintf("%s %d", e, r.Count))
}
embed.Fields = append(embed.Fields, discord.EmbedField{
Name: "Reactions",
Value: strings.Join(parts, " | "),
})
}
embeds = append(embeds, embed)
return
}
func (s *Starboard) updateStarboardEntry(starboardMsgIDStr string, msg *discord.Message, channelID, guildID, messageID snowflake.ID) {
content, embeds, files := s.buildStarboardMessage(msg, channelID, guildID, messageID)
starboardMsgID, err := snowflake.Parse(starboardMsgIDStr)
if err != nil {
slog.Error("failed to parse starboard message ID", slog.Any("err", err))
return
}
emptyAttachments := []discord.AttachmentUpdate{}
update := discord.WebhookMessageUpdate{
Content: &content,
Embeds: &embeds,
Attachments: &emptyAttachments,
Files: files,
}
if _, err := s.webhook.UpdateMessage(starboardMsgID, update, rest.UpdateWebhookMessageParams{}); err != nil {
slog.Error("failed to update starboard webhook", slog.Any("err", err))
}
}
func (s *Starboard) removeStarboardEntry(starboardMsgIDStr, originalMsgID string) {
starboardMsgID, err := snowflake.Parse(starboardMsgIDStr)
if err == nil {
if err := s.webhook.Rest.DeleteWebhookMessage(s.webhook.ID, s.webhook.Token, starboardMsgID, 0); err != nil {
slog.Error("failed to delete starboard webhook message", slog.Any("err", err))
}
} else {
slog.Error("failed to parse starboard message ID for deletion", slog.Any("err", err))
}
if err := s.db.RemoveStarboardEntry(originalMsgID); err != nil {
slog.Error("failed to remove starboard entry", slog.Any("err", err))
}
}