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") }