Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make bot ready to allow independent announcements in multiple servers #16

Merged
merged 10 commits into from
Dec 26, 2024
9 changes: 0 additions & 9 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ discord:
name: Cake4Everybot
credits: Cake4Everybot, developed by @Kesuaheli (Discord) and the ideas of the community ♥

announce:
# The channels ID's to subscribe to
# Its sort of a whitelist. Each channel in this list is allowed to send
# announcement events through the bot
youtube:
- UC6sb0bkXREewXp2AkSOsOqg # Taomi
twitch:
- "404257324" # Taomi_

event:
# Time (24h format) to trigger daily events like birthday check and advent calendar post
morning_hour: 8
Expand Down
2 changes: 0 additions & 2 deletions data/lang/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,9 @@ module:
twitch:
embed_category: Kategorie
embed_footer: Twitch Glocke
msg.nofification: "%s ist auf Twitch live gegangen!"

youtube:
embed_footer: YouTube Glocke
msg.new_vid: "%s hat ein neues Video hochgeladen"

twitch.command:
generic:
Expand Down
2 changes: 0 additions & 2 deletions data/lang/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,9 @@ module:
twitch:
embed_category: Category
embed_footer: Twitch notification bell
msg.nofification: "%s went live on Twitch!"

youtube:
embed_footer: YouTube notification bell
msg.new_vid: "%s just uploaded a new video"

twitch.command:
generic:
Expand Down
45 changes: 34 additions & 11 deletions database/announcements.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ package database
//
// It can be obtained by GetAnnouncement for a given channel on a platform.
type Announcement struct {
GuildID string
ChannelID string
MessageID string
RoleID string
Platform Platform
PlatformID string
GuildID string
ChannelID string
MessageID string
RoleID string
Platform Platform
PlatformID string
Notification string
}

// Platform is the type of platform a Announcement can be made
Expand Down Expand Up @@ -50,34 +51,56 @@ func (p Platform) GoString() string {
}
}

// GetAllAnnouncementIDs returns all platform IDs for a given platform.
//
// If no result matches the given platform the returned error will be sql.ErrNoRows.
// Other errors may exist.
func GetAllAnnouncementIDs(platform Platform) (platformIDs []string, err error) {
rows, err := Query("SELECT DISTINCT platform_id FROM announcements WHERE platform=?", platform)
if err != nil {
return nil, err
}
defer rows.Close()

platformIDs = make([]string, 0)
for rows.Next() {
var platformID string
if err := rows.Scan(&platformID); err != nil {
return nil, err
}
platformIDs = append(platformIDs, platformID)
}
return platformIDs, nil
}

// GetAnnouncement reads all Discord announcement channels from the database for a given channel ID
// on a platform.
// A platform could be "twitch" or "youtube".
//
// If no result matches the given platform and channel ID the returned error will be sql.ErrNoRows.
// Other errors may exist.
func GetAnnouncement(platform Platform, platformID string) ([]*Announcement, error) {
rows, err := Query("SELECT guild_id,channel_id,message_id,role_id FROM announcements WHERE platform=? AND platform_id=?", platform, platformID)
rows, err := Query("SELECT guild_id,channel_id,message_id,role_id,notification FROM announcements WHERE platform=? AND platform_id=?", platform, platformID)
if err != nil {
return []*Announcement{}, err
}
defer rows.Close()
announcements := make([]*Announcement, 0)
for rows.Next() {
var guildID, channelID, messageID, roleID string
var guildID, channelID, messageID, roleID, notification string
if err := rows.Scan(&guildID, &channelID, &messageID, &roleID); err != nil {
return []*Announcement{}, err
}
announcements = append(announcements, &Announcement{guildID, channelID, messageID, roleID, platform, platformID})
announcements = append(announcements, &Announcement{guildID, channelID, messageID, roleID, platform, platformID, notification})
}
return announcements, err
}

// UpdateAnnouncementMessage updates the message id of a with newID.
func (a *Announcement) UpdateAnnouncementMessage(newID string) error {
_, err := Exec("UPDATE announcements SET message_id=? WHERE guild_id=? AND channel_id=? AND message_id=? AND role_id=? AND platform=? AND platform_id=?",
_, err := Exec("UPDATE announcements SET message_id=? WHERE guild_id=? AND channel_id=? AND message_id=? AND role_id=? AND platform=? AND platform_id=? AND notification=?",
newID,
a.GuildID, a.ChannelID, a.MessageID, a.RoleID, a.Platform, a.PlatformID,
a.GuildID, a.ChannelID, a.MessageID, a.RoleID, a.Platform, a.PlatformID, a.Notification,
)
a.MessageID = newID
return err
Expand Down
38 changes: 22 additions & 16 deletions database/giveaway.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ type GiveawayEntry struct {
Weight int
// The day of last entry. Useful to check when only one ticket per day is allowed.
LastEntry time.Time
// The platform the giveaway is for
Platform Platform
// The platform identifier e.g. the channel id for the platform
PlatformID string
}

// ToEmbedField formats the giveaway entry to an discord message embed field.
Expand All @@ -59,22 +63,22 @@ func (e GiveawayEntry) ToEmbedField(s *discordgo.Session, totalTickets int) (f *
// prefixed with prefix.
//
// If an error occours or it doesn't match prefix, an emtpy GiveawayEntry is returned instead.
func GetGiveawayEntry(prefix, userID string) GiveawayEntry {
func GetGiveawayEntry(prefix, userID string, platform Platform, platformID string) GiveawayEntry {
var (
weight int
lastEntryID string
)
err := QueryRow("SELECT weight,last_entry_id FROM giveaway WHERE id=?", userID).Scan(&weight, &lastEntryID)
err := QueryRow("SELECT weight,last_entry_id FROM giveaway WHERE id=? AND platform=? AND platform_id=?", userID, platform, platformID).Scan(&weight, &lastEntryID)
if err == sql.ErrNoRows {
return GiveawayEntry{UserID: userID, Weight: 0}
return GiveawayEntry{UserID: userID, Weight: 0, Platform: platform, PlatformID: platformID}
}
if err != nil {
log.Printf("Database failed to get giveaway entries for '%s': %v", userID, err)
return GiveawayEntry{}
}

if lastEntryID == "" {
return GiveawayEntry{UserID: userID, Weight: weight}
return GiveawayEntry{UserID: userID, Weight: weight, Platform: platform, PlatformID: platformID}
}

dateValue, ok := strings.CutPrefix(lastEntryID, prefix+"-")
Expand All @@ -87,16 +91,16 @@ func GetGiveawayEntry(prefix, userID string) GiveawayEntry {
log.Printf("could not convert last_entry_id '%s' to time: %v", lastEntryID, err)
return GiveawayEntry{}
}
return GiveawayEntry{userID, weight, lastEntry}
return GiveawayEntry{userID, weight, lastEntry, platform, platformID}
}

// DeleteGiveawayEntry deletes the giveaway entry for the given user identifier from the database.
//
// If an error occours it will be returned. However if no datbase entry matched it returns err ==
// nil, not err == sql.ErrNoRows. Because sql.ErrNoRows also results in the non-existence of the
// requested row and therefore is treated as a successful call.
func DeleteGiveawayEntry(userID string) error {
_, err := Exec("DELETE FROM giveaway WHERE id=?", userID)
func DeleteGiveawayEntry(userID string, platform Platform, platformID string) error {
_, err := Exec("DELETE FROM giveaway WHERE id=? AND platform=? AND platform_id=?", userID, platform, platformID)
if err == sql.ErrNoRows {
return nil
}
Expand All @@ -110,13 +114,13 @@ func DeleteGiveawayEntry(userID string) error {
//
// If there was no error the modified entry is returned. If there was an error, an emtpy
// GiveawayEntry is returned instead.
func AddGiveawayWeight(prefix, userID string, amount int) GiveawayEntry {
func AddGiveawayWeight(prefix, userID string, amount int, platform Platform, platformID string) GiveawayEntry {
var (
weight int
lastEntryID string
new bool
)
err := QueryRow("SELECT weight,last_entry_id FROM giveaway WHERE id=?", userID).Scan(&weight, &lastEntryID)
err := QueryRow("SELECT weight,last_entry_id FROM giveaway WHERE id=? AND platform=? AND platform_id=?", userID, platform, platformID).Scan(&weight, &lastEntryID)
if err == sql.ErrNoRows {
new = true
} else if err != nil {
Expand All @@ -140,20 +144,22 @@ func AddGiveawayWeight(prefix, userID string, amount int) GiveawayEntry {
log.Printf("Database failed to insert giveaway for '%s': %v", userID, err)
return GiveawayEntry{}
}
return GiveawayEntry{userID, weight, lastEntry}
return GiveawayEntry{userID, weight, lastEntry, platform, platformID}
}
_, err = Exec("UPDATE giveaway SET weight=?,last_entry_id=? WHERE id=?", weight, lastEntryID, userID)
if err != nil {
log.Printf("Database failed to update weight (new: %d) for '%s': %v", weight, userID, err)
return GiveawayEntry{}
}
return GiveawayEntry{userID, weight, lastEntry}
return GiveawayEntry{userID, weight, lastEntry, platform, platformID}
}

// GetAllGiveawayEntries gets all giveaway entries that matches prefix.
func GetAllGiveawayEntries(prefix string) []GiveawayEntry {
rows, err := Query("SELECT id,weight,last_entry_id FROM giveaway")
if err != nil {
func GetAllGiveawayEntries(prefix string, platform Platform, platformID string) []GiveawayEntry {
rows, err := Query("SELECT id,weight,last_entry_id FROM giveaway WHERE platform=? AND platform_id=?", platform, platformID)
if err == sql.ErrNoRows {
return []GiveawayEntry{}
} else if err != nil {
log.Printf("ERROR: could not get entries from database: %v", err)
return []GiveawayEntry{}
}
Expand All @@ -173,7 +179,7 @@ func GetAllGiveawayEntries(prefix string) []GiveawayEntry {
}

if lastEntryID == "" {
entries = append(entries, GiveawayEntry{UserID: userID, Weight: weight})
entries = append(entries, GiveawayEntry{UserID: userID, Weight: weight, Platform: platform, PlatformID: platformID})
continue
}

Expand All @@ -187,7 +193,7 @@ func GetAllGiveawayEntries(prefix string) []GiveawayEntry {
log.Printf("ERROR: could not convert last_entry_id '%s' to time: %v", lastEntryID, err)
continue
}
entries = append(entries, GiveawayEntry{userID, weight, lastEntry})
entries = append(entries, GiveawayEntry{userID, weight, lastEntry, platform, platformID})
}
return entries
}
Expand Down
2 changes: 1 addition & 1 deletion event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ func AddListeners(dc *discordgo.Session, t *twitchgo.Session, webChan chan struc
t.OnChannelMessage(twitch.MessageHandler)

addYouTubeListeners(dc)
addTwitchListeners(dc, t)
addTwitchListeners(dc, t, webChan)
addScheduledTriggers(dc, t, webChan)
}
7 changes: 4 additions & 3 deletions event/scheduledTriggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,23 @@ import (
)

func addScheduledTriggers(dc *discordgo.Session, t *twitchgo.Session, webChan chan struct{}) {
go scheduleFunction(dc, t, 0, 0,
go scheduleFunction(dc, t, 0, 0, webChan,
adventcalendar.Midnight,
)

go scheduleFunction(dc, t, viper.GetInt("event.morning_hour"), viper.GetInt("event.morning_minute"),
go scheduleFunction(dc, t, viper.GetInt("event.morning_hour"), viper.GetInt("event.morning_minute"), webChan,
birthday.Check,
adventcalendar.Post,
)

go refreshYoutube(webChan)
}

func scheduleFunction(dc *discordgo.Session, t *twitchgo.Session, hour, min int, callbacks ...interface{}) {
func scheduleFunction(dc *discordgo.Session, t *twitchgo.Session, hour, min int, webChan chan struct{}, callbacks ...interface{}) {
if len(callbacks) == 0 {
return
}
<-webChan
log.Printf("scheduled %d function(s) for %2d:%02d!", len(callbacks), hour, min)
time.Sleep(time.Second * 5)
for {
Expand Down
24 changes: 15 additions & 9 deletions event/twitch/announce.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,22 @@ import (

// HandleChannelUpdate is the event handler for the "channel.update" event from twitch.
func HandleChannelUpdate(s *discordgo.Session, t *twitchgo.Session, e *webTwitch.ChannelUpdateEvent) {
HandleStreamAnnouncementChange(s, t, e.BroadcasterUserID, e.Title, "")
HandleStreamAnnouncementChange(s, t, e.BroadcasterUserID, e.Title, false)
}

// HandleStreamOnline is the event handler for the "stream.online" event from twitch.
func HandleStreamOnline(s *discordgo.Session, t *twitchgo.Session, e *webTwitch.StreamOnlineEvent) {
HandleStreamAnnouncementChange(s, t, e.BroadcasterUserID, "", lang.GetDefault("module.twitch.msg.nofification"))
HandleStreamAnnouncementChange(s, t, e.BroadcasterUserID, "", true)
}

// HandleStreamOffline is the event handler for the "stream.offline" event from twitch.
func HandleStreamOffline(s *discordgo.Session, t *twitchgo.Session, e *webTwitch.StreamOfflineEvent) {
HandleStreamAnnouncementChange(s, t, e.BroadcasterUserID, "", "")
HandleStreamAnnouncementChange(s, t, e.BroadcasterUserID, "", false)
}

// HandleStreamAnnouncementChange is a general event handler for twitch events, that should update
// the discord announcement embed.
func HandleStreamAnnouncementChange(s *discordgo.Session, t *twitchgo.Session, platformID, title, notification string) {
func HandleStreamAnnouncementChange(s *discordgo.Session, t *twitchgo.Session, platformID, title string, sendNotification bool) {
announcements, err := database.GetAnnouncement(database.AnnouncementPlatformTwitch, platformID)
if err == sql.ErrNoRows {
return
Expand All @@ -42,7 +42,7 @@ func HandleStreamAnnouncementChange(s *discordgo.Session, t *twitchgo.Session, p
}

for _, announcement := range announcements {
err = updateAnnouncementMessage(s, t, announcement, title, notification)
err = updateAnnouncementMessage(s, t, announcement, title, sendNotification)
if err != nil {
log.Printf("Error: %v", err)
}
Expand Down Expand Up @@ -72,7 +72,7 @@ func newAnnouncementMessage(s *discordgo.Session, announcement *database.Announc
return msg, announcement.UpdateAnnouncementMessage(msg.ID)
}

func updateAnnouncementMessage(s *discordgo.Session, t *twitchgo.Session, announcement *database.Announcement, title, notification string) error {
func updateAnnouncementMessage(s *discordgo.Session, t *twitchgo.Session, announcement *database.Announcement, title string, sendNotification bool) error {
msg, err := getAnnouncementMessage(s, announcement)
if err != nil {
return fmt.Errorf("get announcement in channel '%s': %v", announcement, err)
Expand Down Expand Up @@ -114,11 +114,17 @@ func updateAnnouncementMessage(s *discordgo.Session, t *twitchgo.Session, announ
setOfflineEmbed(embed, user)
}

if notification != "" {
if sendNotification {
notificationContent := announcement.Notification
if announcement.Notification == "" {
notificationContent = user.DisplayName
} else if strings.Contains(announcement.Notification, "%s") {
notificationContent = fmt.Sprintf(announcement.Notification, user.DisplayName)
}
if announcement.RoleID != "" {
notification += fmt.Sprintf("\n<@&%s>", announcement.RoleID)
notificationContent += (&discordgo.Role{ID: announcement.RoleID}).Mention()
}
msgNotification, err := s.ChannelMessageSend(announcement.ChannelID, fmt.Sprintf(notification, user.DisplayName))
msgNotification, err := s.ChannelMessageSend(announcement.ChannelID, notificationContent)
if err != nil {
return fmt.Errorf("send notification: %v", err)
}
Expand Down
12 changes: 6 additions & 6 deletions event/twitch/messageHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const tp string = "twitch.command."
// MessageHandler handles new messages from the twitch chat(s). It will be called on every new
// message.
func MessageHandler(t *twitchgo.Session, channel string, user *twitchgo.IRCUser, message, msgID string, tags twitchgo.IRCMessageTags) {
log.Printf("<%s@%s> %s", user.Nickname, channel, message)
//log.Printf("<%s@%s> %s", user.Nickname, channel, message)
}

// HandleCmdJoin is the handler for a command in a twitch chat. This handler buys a giveaway ticket
Expand All @@ -56,7 +56,7 @@ func HandleCmdJoin(t *twitchgo.Session, channel string, user *twitchgo.IRCUser,
t.SendMessagef(channel, lang.GetDefault(tp+"msg.won"), user.Nickname)
return
}
entry := database.GetGiveawayEntry("tw11", user.Nickname)
entry := database.GetGiveawayEntry("tw11", user.Nickname, database.AnnouncementPlatformTwitch, channel) // FIXME: use channel ID instead of name
if entry.UserID == "" {
log.Printf("Error getting database giveaway entry: %v", err)
t.SendMessage(channel, lang.GetDefault("twitch.command.generic.error"))
Expand Down Expand Up @@ -118,7 +118,7 @@ func HandleCmdJoin(t *twitchgo.Session, channel string, user *twitchgo.IRCUser,
t.SendMessagef(channel, lang.GetDefault(tp+"msg.too_few_points"), user.Nickname, sePoints.Points, joinCost-sePoints.Points, joinCost)
return
}
entry = database.AddGiveawayWeight("tw11", user.Nickname, 1)
entry = database.AddGiveawayWeight("tw11", user.Nickname, 1, database.AnnouncementPlatformTwitch, channel) // FIXME: use channel ID instead of name
if entry.UserID == "" {
log.Printf("Error getting database giveaway entry: %v", err)
t.SendMessage(channel, lang.GetDefault("twitch.command.generic.error"))
Expand Down Expand Up @@ -176,7 +176,7 @@ func HandleCmdTickets(t *twitchgo.Session, channel string, source *twitchgo.IRCU
return
}

entry := database.GetGiveawayEntry("tw11", userID)
entry := database.GetGiveawayEntry("tw11", userID, database.AnnouncementPlatformTwitch, channel) // FIXME: use channel ID instead of name
if entry.Weight >= 10 {
if source.Nickname == userID {
t.SendMessagef(channel, lang.GetDefault(tp+"msg.max_tickets"), source.Nickname)
Expand Down Expand Up @@ -270,15 +270,15 @@ func HandleCmdDraw(t *twitchgo.Session, channel string, user *twitchgo.IRCUser,
return
}

winner, totalTickets := database.DrawGiveawayWinner(database.GetAllGiveawayEntries("tw11"))
winner, totalTickets := database.DrawGiveawayWinner(database.GetAllGiveawayEntries("tw11", database.AnnouncementPlatformTwitch, channel)) // FIXME: use channel ID instead of name
if totalTickets == 0 {
t.SendMessagef(channel, lang.GetDefault(tp+"msg.no_entries"), user.Nickname)
return
}

t.SendMessagef(channel, lang.GetDefault(tp+"msg.winner"), winner.UserID, prize.Name, winner.Weight, float64(winner.Weight*100)/float64(totalTickets))

err = database.DeleteGiveawayEntry(winner.UserID)
err = database.DeleteGiveawayEntry(winner.UserID, database.AnnouncementPlatformTwitch, channel) // FIXME: use channel ID instead of name
if err != nil {
log.Printf("Error deleting database giveaway entry: %v", err)
t.SendMessage(channel, lang.GetDefault("twitch.command.generic.error"))
Expand Down
Loading
Loading