Skip to content

Commit b02090c

Browse files
authored
Merge pull request #18 from cake4everyone/feat/per-server-birthdays
Added per server announcements for a birthday
2 parents bea388e + e23f17f commit b02090c

6 files changed

+151
-50
lines changed

modules/birthday/birthdaybase.go

+103-30
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ type birthdayBase struct {
4343
}
4444

4545
type birthdayEntry struct {
46-
ID uint64 `database:"id"`
47-
Day int `database:"day"`
48-
Month int `database:"month"`
49-
Year int `database:"year"`
50-
Visible bool `database:"visible"`
51-
time time.Time
46+
ID uint64 `database:"id"`
47+
Day int `database:"day"`
48+
Month int `database:"month"`
49+
Year int `database:"year"`
50+
Visible bool `database:"visible"`
51+
time time.Time
52+
GuildIDsRaw string `database:"guilds"`
53+
GuildIDs []string
5254
}
5355

5456
// Returns a readable Form of the date
@@ -108,15 +110,75 @@ func (b birthdayEntry) Age() int {
108110
return b.Next().Year() - b.Year - 1
109111
}
110112

113+
// ParseGuildIDs splits the guild IDs into a slice and stores them in b.GuildIDs.
114+
func (b *birthdayEntry) ParseGuildIDs() {
115+
b.GuildIDs = strings.Split(b.GuildIDsRaw, ",")
116+
}
117+
118+
// IsInGuild returns true if the guildID is in b.GuildIDs.
119+
// If guildID is empty, IsInGuild returns true.
120+
func (b birthdayEntry) IsInGuild(guildID string) bool {
121+
if guildID == "" {
122+
return true
123+
}
124+
return util.ContainsString(b.GuildIDs, guildID)
125+
}
126+
127+
// SetGuild sets the guildID in the birthday entry.
128+
func (b *birthdayEntry) SetGuild(guildID string) {
129+
b.GuildIDsRaw += guildID
130+
b.ParseGuildIDs()
131+
}
132+
133+
// AddGuild adds the guildID to the birthday entry.
134+
func (b *birthdayEntry) AddGuild(guildID string) error {
135+
if util.ContainsString(b.GuildIDs, guildID) {
136+
return nil
137+
} else if len(b.GuildIDs) >= 3 {
138+
return fmt.Errorf("this entry already has %d guilds", len(b.GuildIDs))
139+
}
140+
b.GuildIDsRaw += "," + guildID
141+
b.GuildIDsRaw = strings.Trim(b.GuildIDsRaw, ", ")
142+
b.ParseGuildIDs()
143+
return nil
144+
}
145+
146+
// IsEqual returns true if b and b2 are equal.
147+
//
148+
// That is, if all of the following are true
149+
// 1. They have the same user ID.
150+
// 2. They are on the same date.
151+
// 3. They have the same visibility.
152+
// 4. They have the same guilds in (any order).
153+
func (b birthdayEntry) IsEqual(b2 birthdayEntry) bool {
154+
if b.ID != b2.ID || b.Day != b2.Day || b.Month != b2.Month || b.Year != b2.Year || b.Visible != b2.Visible {
155+
return false
156+
}
157+
158+
// check for same guilds in any order
159+
for _, guildID := range b.GuildIDs {
160+
if !util.ContainsString(b2.GuildIDs, guildID) {
161+
return false
162+
}
163+
}
164+
for _, guildID := range b2.GuildIDs {
165+
if !util.ContainsString(b.GuildIDs, guildID) {
166+
return false
167+
}
168+
}
169+
return true
170+
}
171+
111172
// getBirthday copies all birthday fields into the struct pointed at by b.
112173
//
113174
// If the user from b.ID is not found it returns sql.ErrNoRows.
114175
func (cmd birthdayBase) getBirthday(b *birthdayEntry) (err error) {
115-
row := database.QueryRow("SELECT day,month,year,visible FROM birthdays WHERE id=?", b.ID)
116-
err = row.Scan(&b.Day, &b.Month, &b.Year, &b.Visible)
176+
row := database.QueryRow("SELECT day,month,year,visible,guilds FROM birthdays WHERE id=?", b.ID)
177+
err = row.Scan(&b.Day, &b.Month, &b.Year, &b.Visible, &b.GuildIDsRaw)
117178
if err != nil {
118179
return err
119180
}
181+
b.ParseGuildIDs()
120182
return b.ParseTime()
121183
}
122184

@@ -127,27 +189,36 @@ func (cmd birthdayBase) hasBirthday(id uint64) (hasBirthday bool, err error) {
127189
}
128190

129191
// setBirthday inserts a new database entry with the values from b.
130-
func (cmd birthdayBase) setBirthday(b birthdayEntry) error {
131-
_, err := database.Exec("INSERT INTO birthdays(id,day,month,year,visible) VALUES(?,?,?,?,?);", b.ID, b.Day, b.Month, b.Year, b.Visible)
192+
func (cmd birthdayBase) setBirthday(b *birthdayEntry) (err error) {
193+
b.SetGuild(cmd.Interaction.GuildID)
194+
_, err = database.Exec("INSERT INTO birthdays(id,day,month,year,visible,guilds) VALUES(?,?,?,?,?);", b.ID, b.Day, b.Month, b.Year, b.Visible, b.GuildIDsRaw)
132195
return err
133196
}
134197

135198
// updateBirthday updates an existing database entry with the values from b.
136-
func (cmd birthdayBase) updateBirthday(b birthdayEntry) (before birthdayEntry, err error) {
137-
err = b.ParseTime()
138-
if err != nil {
139-
return birthdayEntry{}, err
140-
}
199+
func (cmd birthdayBase) updateBirthday(b *birthdayEntry) (before birthdayEntry, err error) {
141200
before.ID = b.ID
142201
if err = cmd.getBirthday(&before); err != nil {
143202
return birthdayEntry{}, fmt.Errorf("trying to get old birthday: %v", err)
144203
}
204+
b.GuildIDsRaw = before.GuildIDsRaw
205+
b.ParseGuildIDs()
206+
207+
err = b.AddGuild(cmd.Interaction.GuildID)
208+
if err != nil {
209+
return birthdayEntry{}, fmt.Errorf("adding guild '%s' to birthday entry: %v", cmd.Interaction.GuildID, err)
210+
}
211+
212+
// early return if nothing changed
213+
if b.IsEqual(before) {
214+
return before, nil
215+
}
145216

146217
var (
147218
updateNames []string
148219
updateVars []any
149220
oldV reflect.Value = reflect.ValueOf(before)
150-
v reflect.Value = reflect.ValueOf(b)
221+
v reflect.Value = reflect.ValueOf(*b)
151222
)
152223
for i := 0; i < v.NumField(); i++ {
153224
var (
@@ -160,11 +231,11 @@ func (cmd birthdayBase) updateBirthday(b birthdayEntry) (before birthdayEntry, e
160231
continue
161232
}
162233

234+
tag := v.Type().Field(i).Tag.Get("database")
235+
if tag == "" {
236+
continue
237+
}
163238
if f.Interface() != oldF.Interface() {
164-
tag := v.Type().Field(i).Tag.Get("database")
165-
if tag == "" {
166-
continue
167-
}
168239
updateNames = append(updateNames, tag)
169240
updateVars = append(updateVars, f.Interface())
170241
}
@@ -193,8 +264,8 @@ func (cmd birthdayBase) removeBirthday(id uint64) (birthdayEntry, error) {
193264
return b, err
194265
}
195266

196-
// getBirthdaysMonth return a sorted slice of birthday entries that matches the given month.
197-
func (cmd birthdayBase) getBirthdaysMonth(month int) (birthdays []birthdayEntry, err error) {
267+
// getBirthdaysMonth return a sorted slice of birthday entries that matches the given guildID and month.
268+
func (cmd birthdayBase) getBirthdaysMonth(guildID string, month int) (birthdays []birthdayEntry, err error) {
198269
var numOfEntries int64
199270
err = database.QueryRow("SELECT COUNT(*) FROM birthdays WHERE month=?", month).Scan(&numOfEntries)
200271
if err != nil {
@@ -206,20 +277,21 @@ func (cmd birthdayBase) getBirthdaysMonth(month int) (birthdays []birthdayEntry,
206277
return birthdays, nil
207278
}
208279

209-
rows, err := database.Query("SELECT id,day,year,visible FROM birthdays WHERE month=?", month)
280+
rows, err := database.Query("SELECT id,day,year,visible,guilds FROM birthdays WHERE month=?", month)
210281
if err != nil {
211282
return birthdays, err
212283
}
213284
defer rows.Close()
214285

215286
for rows.Next() {
216287
b := birthdayEntry{Month: month}
217-
err = rows.Scan(&b.ID, &b.Day, &b.Year, &b.Visible)
288+
err = rows.Scan(&b.ID, &b.Day, &b.Year, &b.Visible, &b.GuildIDsRaw)
218289
if err != nil {
219290
return birthdays, err
220291
}
292+
b.ParseGuildIDs()
221293

222-
if !b.Visible {
294+
if !b.Visible || !b.IsInGuild(guildID) {
223295
continue
224296
}
225297

@@ -238,8 +310,8 @@ func (cmd birthdayBase) getBirthdaysMonth(month int) (birthdays []birthdayEntry,
238310
return birthdays, nil
239311
}
240312

241-
// getBirthdaysDate return a slice of birthday entries that matches the given date.
242-
func getBirthdaysDate(day int, month int) (birthdays []birthdayEntry, err error) {
313+
// getBirthdaysDate return a slice of birthday entries that matches the given guildID and date.
314+
func getBirthdaysDate(guildID string, day int, month int) (birthdays []birthdayEntry, err error) {
243315
var numOfEntries int64
244316
err = database.QueryRow("SELECT COUNT(*) FROM birthdays WHERE day=? AND month=?", day, month).Scan(&numOfEntries)
245317
if err != nil {
@@ -251,20 +323,21 @@ func getBirthdaysDate(day int, month int) (birthdays []birthdayEntry, err error)
251323
return birthdays, nil
252324
}
253325

254-
rows, err := database.Query("SELECT id,year,visible FROM birthdays WHERE day=? AND month=?", day, month)
326+
rows, err := database.Query("SELECT id,year,visible,guilds FROM birthdays WHERE day=? AND month=?", day, month)
255327
if err != nil {
256328
return birthdays, err
257329
}
258330
defer rows.Close()
259331

260332
for rows.Next() {
261333
b := birthdayEntry{Day: day, Month: month}
262-
err = rows.Scan(&b.ID, &b.Year, &b.Visible)
334+
err = rows.Scan(&b.ID, &b.Year, &b.Visible, &b.GuildIDsRaw)
263335
if err != nil {
264336
return birthdays, err
265337
}
338+
b.ParseGuildIDs()
266339

267-
if !b.Visible {
340+
if !b.Visible || !b.IsInGuild(guildID) {
268341
continue
269342
}
270343

modules/birthday/handleCheck.go

+19-12
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,16 @@ func Check(s *discordgo.Session) {
3535
defer rows.Close()
3636

3737
now := time.Now()
38-
birthdays, err := getBirthdaysDate(now.Day(), int(now.Month()))
39-
if err != nil {
40-
log.Printf("Error on getting todays birthdays from database: %v\n", err)
41-
}
42-
e, n := birthdayAnnounceEmbed(s, birthdays)
43-
if n <= 0 {
44-
return
45-
}
4638

4739
for rows.Next() {
4840
err = rows.Scan(&guildID, &channelID)
4941
if err != nil {
5042
log.Printf("Error on scanning birthday channel ID from database %v\n", err)
5143
continue
5244
}
45+
if channelID == 0 {
46+
continue
47+
}
5348

5449
channel, err := s.Channel(fmt.Sprint(channelID))
5550
if err != nil {
@@ -61,6 +56,15 @@ func Check(s *discordgo.Session) {
6156
return
6257
}
6358

59+
birthdays, err := getBirthdaysDate(fmt.Sprint(guildID), now.Day(), int(now.Month()))
60+
if err != nil {
61+
log.Printf("Error on getting todays birthdays from guild %s from database: %v\n", fmt.Sprint(guildID), err)
62+
}
63+
e, n := birthdayAnnounceEmbed(s, fmt.Sprint(guildID), birthdays)
64+
if n <= 0 {
65+
return
66+
}
67+
6468
// announce
6569
_, err = s.ChannelMessageSendEmbed(channel.ID, e)
6670
if err != nil {
@@ -71,7 +75,7 @@ func Check(s *discordgo.Session) {
7175

7276
// birthdayAnnounceEmbed returns the embed, that contains all birthdays and 'n' as the number of
7377
// birthdays, which is always len(b)
74-
func birthdayAnnounceEmbed(s *discordgo.Session, b []birthdayEntry) (e *discordgo.MessageEmbed, n int) {
78+
func birthdayAnnounceEmbed(s *discordgo.Session, guildID string, b []birthdayEntry) (e *discordgo.MessageEmbed, n int) {
7579
var title, fValue string
7680

7781
switch len(b) {
@@ -85,14 +89,17 @@ func birthdayAnnounceEmbed(s *discordgo.Session, b []birthdayEntry) (e *discordg
8589
}
8690

8791
for _, b := range b {
88-
mention := fmt.Sprintf("<@%d>", b.ID)
92+
member := util.IsGuildMember(s, guildID, fmt.Sprint(b.ID))
93+
if member == nil {
94+
continue
95+
}
8996

9097
if b.Year == 0 {
91-
fValue += fmt.Sprintf("%s\n", mention)
98+
fValue += fmt.Sprintf("%s\n", member.Mention())
9299
} else {
93100
format := lang.Get(tp+"msg.announce.with_age", lang.FallbackLang())
94101
format += "\n"
95-
fValue += fmt.Sprintf(format, mention, fmt.Sprint(b.Age()))
102+
fValue += fmt.Sprintf(format, member.Mention(), fmt.Sprint(b.Age()))
96103
}
97104
}
98105

modules/birthday/handlerSubcommandAnnounce.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ func (cmd Chat) subcommandAnnounce() subcommandAnnounce {
3737

3838
func (cmd subcommandAnnounce) handler() {
3939
now := time.Now()
40-
b, err := getBirthdaysDate(now.Day(), int(now.Month()))
40+
b, err := getBirthdaysDate(cmd.Interaction.GuildID, now.Day(), int(now.Month()))
4141
if err != nil {
42-
log.Printf("Error on announce birthday: %v\n", err)
42+
log.Printf("Error on announce birthday in guild %s: %v\n", cmd.Interaction.GuildID, err)
4343
cmd.ReplyError()
4444
return
4545
}
4646

47-
e, n := birthdayAnnounceEmbed(cmd.Session, b)
47+
e, n := birthdayAnnounceEmbed(cmd.Session, cmd.Interaction.GuildID, b)
4848

4949
if n <= 0 {
5050
cmd.ReplyHiddenEmbed(e)

modules/birthday/handlerSubcommandList.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ func (cmd subcommandList) handler() {
4949
}
5050
month := int(cmd.month.IntValue())
5151

52-
birthdays, err := cmd.getBirthdaysMonth(month)
52+
birthdays, err := cmd.getBirthdaysMonth(cmd.Interaction.GuildID, month)
5353
if err != nil {
54-
log.Printf("Error on get birthdays by month: %v\n", err)
54+
log.Printf("Error on get birthdays by month from guild %s: %v\n", cmd.Interaction.GuildID, err)
5555
cmd.ReplyError()
5656
return
5757
}

modules/birthday/handlerSubcommandSet.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func (cmd subcommandSet) interactionHandler() {
121121
return
122122
}
123123

124-
b := birthdayEntry{
124+
b := &birthdayEntry{
125125
ID: authorID,
126126
Day: int(cmd.day.IntValue()),
127127
Month: int(cmd.month.IntValue()),
@@ -194,13 +194,13 @@ func (cmd subcommandSet) interactionHandler() {
194194
}
195195

196196
// seperate handler for an update of the birthday
197-
func (cmd subcommandSet) handleUpdate(b birthdayEntry, e *discordgo.MessageEmbed) error {
197+
func (cmd subcommandSet) handleUpdate(b *birthdayEntry, e *discordgo.MessageEmbed) (err error) {
198198
before, err := cmd.updateBirthday(b)
199199
if err != nil {
200200
return err
201201
}
202202

203-
if b == before {
203+
if b.IsEqual(before) {
204204
var age string
205205
if b.Year > 0 {
206206
age = fmt.Sprintf(" (%d)", b.Age()+1)

util/discord.go

+21
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,24 @@ func MessageComplexWebhookEdit(src any) *discordgo.WebhookEdit {
437437
panic("Given source type is not supported: " + fmt.Sprintf("%T", src))
438438
}
439439
}
440+
441+
// IsGuildMember returns the given user as a member of the given guild. If the
442+
// user is not a member of the guild IsGuildMember returns nil.
443+
func IsGuildMember(s *discordgo.Session, guildID, userID string) (member *discordgo.Member) {
444+
member, err := s.State.Member(guildID, userID)
445+
if err == nil {
446+
return member
447+
} else if err != discordgo.ErrStateNotFound {
448+
log.Printf("ERROR: Failed to get guild member from cache (G: %s, U: %s): %v\n", guildID, userID, err)
449+
}
450+
member, err = s.GuildMember(guildID, userID)
451+
if err == nil {
452+
return member
453+
}
454+
455+
var restErr *discordgo.RESTError
456+
if !errors.As(err, &restErr) || restErr.Response.StatusCode != http.StatusNotFound {
457+
log.Printf("ERROR: Failed to get guild member from API (G: %s, U: %s): %v\n", guildID, userID, err)
458+
}
459+
return nil
460+
}

0 commit comments

Comments
 (0)