253 lines
6.6 KiB
Go
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))
|
|
}
|
|
}
|