unbot/features/iconchanger.go
2026-06-01 18:37:01 +02:00

207 lines
4.2 KiB
Go

package features
import (
"io"
"log/slog"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"github.com/disgoorg/disgo/bot"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/events"
"github.com/disgoorg/omit"
"github.com/disgoorg/snowflake/v2"
)
type IconChanger struct {
client *bot.Client
guildID snowflake.ID
channelID snowflake.ID
interval time.Duration
enabled bool
mu sync.RWMutex
imageURLs []string
stop chan struct{}
}
func NewIconChanger(client *bot.Client, guildID, channelID uint64, interval time.Duration, enabled bool) *IconChanger {
return &IconChanger{
client: client,
guildID: snowflake.ID(guildID),
channelID: snowflake.ID(channelID),
interval: interval,
enabled: enabled,
stop: make(chan struct{}),
}
}
func (ic *IconChanger) HandleMessage(e *events.MessageCreate) {
if !ic.enabled || e.ChannelID != ic.channelID || e.Message.Author.Bot {
return
}
for _, a := range e.Message.Attachments {
if a.ContentType != nil && strings.HasPrefix(*a.ContentType, "image/") {
ic.mu.Lock()
ic.imageURLs = append(ic.imageURLs, a.URL)
ic.mu.Unlock()
}
}
for _, embed := range e.Message.Embeds {
if embed.Image != nil {
ic.mu.Lock()
ic.imageURLs = append(ic.imageURLs, embed.Image.URL)
ic.mu.Unlock()
}
}
}
func (ic *IconChanger) Start() {
if !ic.enabled {
return
}
interval := ic.interval
if interval <= 0 {
interval = 1 * time.Hour
}
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ic.changeIcon()
case <-ic.stop:
return
}
}
}()
}
func (ic *IconChanger) Stop() {
close(ic.stop)
}
func (ic *IconChanger) BackfillHistory() {
if !ic.enabled {
return
}
slog.Info("starting icon history backfill", slog.String("channel_id", ic.channelID.String()))
var lastID snowflake.ID
seen := make(map[string]struct{})
for {
messages, err := ic.client.Rest.GetMessages(ic.channelID, 0, lastID, 0, 100)
if err != nil {
slog.Error("failed to get messages for icon backfill", slog.Any("err", err))
return
}
if len(messages) == 0 {
break
}
for _, msg := range messages {
if msg.Author.Bot {
continue
}
for _, a := range msg.Attachments {
if a.ContentType != nil && strings.HasPrefix(*a.ContentType, "image/") {
if _, ok := seen[a.URL]; !ok {
seen[a.URL] = struct{}{}
ic.mu.Lock()
ic.imageURLs = append(ic.imageURLs, a.URL)
ic.mu.Unlock()
}
}
}
for _, embed := range msg.Embeds {
if embed.Image != nil {
if _, ok := seen[embed.Image.URL]; !ok {
seen[embed.Image.URL] = struct{}{}
ic.mu.Lock()
ic.imageURLs = append(ic.imageURLs, embed.Image.URL)
ic.mu.Unlock()
}
}
}
}
lastID = messages[len(messages)-1].ID
time.Sleep(200 * time.Millisecond)
}
slog.Info("icon history backfill complete", slog.Int("images_found", len(ic.imageURLs)))
}
func (ic *IconChanger) changeIcon() {
ic.mu.RLock()
urls := make([]string, len(ic.imageURLs))
copy(urls, ic.imageURLs)
ic.mu.RUnlock()
if len(urls) == 0 {
return
}
url := urls[rand.Intn(len(urls))]
icon, err := downloadIcon(url)
if err != nil {
slog.Error("failed to download icon", slog.Any("err", err))
return
}
_, err = ic.client.Rest.UpdateGuild(ic.guildID, discord.GuildUpdate{
Icon: omit.New(icon),
})
if err != nil {
slog.Error("failed to update guild icon", slog.Any("err", err))
return
}
slog.Info("guild icon changed", slog.String("url", url))
}
func downloadIcon(url string) (*discord.Icon, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
mediaType := resp.Header.Get("Content-Type")
if mediaType == "" {
mediaType = "image/png"
}
var iconType discord.IconType
switch {
case mediaType == "image/png":
iconType = discord.IconTypePNG
case mediaType == "image/jpeg":
iconType = discord.IconTypeJPEG
case mediaType == "image/webp":
iconType = discord.IconTypeWEBP
case mediaType == "image/gif":
iconType = discord.IconTypeGIF
default:
iconType = discord.IconTypePNG
}
return discord.NewIconRaw(iconType, data), nil
}