Files
sftrading/rich_presence.go
jbergner 25b42c8335
All checks were successful
release-tag / release-image (push) Successful in 2m8s
Update mit Presence-Einstellungen
2026-05-31 08:34:07 +02:00

193 lines
5.8 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
const richPresenceSettingKey = "rich_presence_config"
func defaultRichPresenceConfig() RichPresenceConfig {
return RichPresenceConfig{
Enabled: false,
Status: "online",
ActivityType: "watching",
Name: "SF Trading",
State: "Gemeinsame Session",
Details: "Bereit für Orga-Aktivitäten",
InviteButtonLabel: "Channel beitreten",
InvitePostTitle: "Gemeinsame Session",
InvitePostText: "Tritt dem Discord-Channel bei und schließe dich der Session an.",
InviteChannelID: "",
InvitePostChannelID: "",
}
}
func (s *Store) GetRichPresenceConfig() (RichPresenceConfig, error) {
cfg := defaultRichPresenceConfig()
raw, err := s.GetSetting(richPresenceSettingKey)
if err != nil {
if err == sql.ErrNoRows {
return cfg, nil
}
return cfg, err
}
if strings.TrimSpace(raw) == "" {
return cfg, nil
}
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return cfg, err
}
if cfg.Status == "" {
cfg.Status = "online"
}
if cfg.ActivityType == "" {
cfg.ActivityType = "watching"
}
if cfg.InviteButtonLabel == "" {
cfg.InviteButtonLabel = "Channel beitreten"
}
if cfg.InvitePostTitle == "" {
cfg.InvitePostTitle = "Gemeinsame Session"
}
return cfg, nil
}
func (s *Store) SaveRichPresenceConfig(cfg RichPresenceConfig) error {
b, err := json.Marshal(cfg)
if err != nil {
return err
}
return s.SetSetting(richPresenceSettingKey, string(b))
}
func (b *Bot) ApplySavedRichPresence() {
cfg, err := b.store.GetRichPresenceConfig()
if err != nil {
log.Println("rich presence config load failed:", err)
return
}
if err := b.SetRichPresence(cfg); err != nil {
log.Println("rich presence apply failed:", err)
}
}
func (b *Bot) SetRichPresence(cfg RichPresenceConfig) error {
if b == nil || b.dg == nil {
return fmt.Errorf("bot session nicht verfügbar")
}
if !cfg.Enabled {
return b.dg.UpdateStatusComplex(discordgo.UpdateStatusData{Status: cfg.Status, Activities: []*discordgo.Activity{}})
}
activity := &discordgo.Activity{
Name: firstNonEmpty(cfg.Name, "SF Trading"),
Type: activityTypeFromString(cfg.ActivityType),
State: strings.TrimSpace(cfg.State),
Details: strings.TrimSpace(cfg.Details),
Assets: discordgo.Assets{
LargeImageID: strings.TrimSpace(cfg.LargeImage),
LargeText: strings.TrimSpace(cfg.LargeText),
SmallImageID: strings.TrimSpace(cfg.SmallImage),
SmallText: strings.TrimSpace(cfg.SmallText),
},
Timestamps: discordgo.TimeStamps{StartTimestamp: cfg.StartTimestamp, EndTimestamp: cfg.EndTimestamp},
}
// Discord beschränkt Bot-Presences offiziell auf name/state/type/url. Details, Assets und Timestamps
// werden mitgesendet, können clientseitig aber ignoriert werden. Der Session-Invite-Post enthält
// deshalb den zuverlässigen Button-Link.
return b.dg.UpdateStatusComplex(discordgo.UpdateStatusData{Status: firstNonEmpty(cfg.Status, "online"), Activities: []*discordgo.Activity{activity}})
}
func (b *Bot) SendRichPresenceInvite(cfg RichPresenceConfig) (*discordgo.Message, error) {
if strings.TrimSpace(cfg.InvitePostChannelID) == "" {
return nil, fmt.Errorf("Invite-Post-Channel fehlt")
}
link := channelLink(b.cfg.GuildID, cfg.InviteChannelID)
if link == "" {
return nil, fmt.Errorf("Invite-Channel fehlt")
}
buttonLabel := firstNonEmpty(cfg.InviteButtonLabel, "Channel beitreten")
emb := &discordgo.MessageEmbed{
Title: firstNonEmpty(cfg.InvitePostTitle, "Gemeinsame Session"),
Description: firstNonEmpty(cfg.InvitePostText, "Tritt dem Discord-Channel bei und schließe dich der Session an."),
Color: 0x38bdf8,
Timestamp: time.Now().Format(time.RFC3339),
Fields: []*discordgo.MessageEmbedField{
{Name: "Channel", Value: fmt.Sprintf("<#%s>", cfg.InviteChannelID), Inline: true},
{Name: "Status", Value: firstNonEmpty(cfg.State, cfg.Details, cfg.Name, "Session aktiv"), Inline: true},
},
}
msg, err := b.dg.ChannelMessageSendComplex(cfg.InvitePostChannelID, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{emb},
Components: []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{discordgo.Button{Label: buttonLabel, Style: discordgo.LinkButton, URL: link}}}},
})
if err != nil {
return nil, err
}
cfg.LastInviteChannelID = msg.ChannelID
cfg.LastInviteMessageID = msg.ID
_ = b.store.SaveRichPresenceConfig(cfg)
return msg, nil
}
func activityTypeFromString(v string) discordgo.ActivityType {
switch strings.ToLower(strings.TrimSpace(v)) {
case "playing", "game", "spielt":
return discordgo.ActivityTypeGame
case "streaming":
return discordgo.ActivityTypeStreaming
case "listening", "hört":
return discordgo.ActivityTypeListening
case "competing":
return discordgo.ActivityTypeCompeting
case "custom":
return discordgo.ActivityTypeCustom
default:
return discordgo.ActivityTypeWatching
}
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
return ""
}
func channelLink(guildID, channelID string) string {
guildID = strings.TrimSpace(guildID)
channelID = strings.TrimSpace(channelID)
if guildID == "" || channelID == "" {
return ""
}
return fmt.Sprintf("https://discord.com/channels/%s/%s", guildID, channelID)
}
func unixMillisFromDatetimeLocal(v string) int64 {
v = strings.TrimSpace(v)
if v == "" {
return 0
}
for _, layout := range []string{"2006-01-02T15:04", "2006-01-02T15:04:05", time.RFC3339} {
if t, err := time.ParseInLocation(layout, v, time.Local); err == nil {
return t.UnixMilli()
}
}
return 0
}
func datetimeLocalFromUnixMillis(ms int64) string {
if ms <= 0 {
return ""
}
return time.UnixMilli(ms).Local().Format("2006-01-02T15:04")
}