Skip to content

Commit 3844752

Browse files
authored
Merge pull request #125 from lazybytez/feature/bot-status
Add bot status management
2 parents cbef507 + 532ba4f commit 3844752

File tree

8 files changed

+358
-0
lines changed

8 files changed

+358
-0
lines changed

api/component.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,17 @@ type ServiceManager interface {
122122
// Prefer using the EntityManager instead, as DatabaseAccess is considered
123123
// a low-level api.
124124
DatabaseAccess() *EntityManager
125+
// BotAuditLogger returns the bot audit logger for the current component,
126+
// which allows to create audit log entries.
127+
BotAuditLogger() *BotAuditLogger
128+
// DiscordApi is used to obtain the components slash DiscordApiWrapper management
129+
//
130+
// On first call, this function initializes the private Component.discordAPi
131+
// field. On consecutive calls, the already present DiscordGoApiWrapper will be used.
132+
DiscordApi() DiscordApiWrapper
133+
// BotStatusManager returns the current StatusManager which
134+
// allows to add additional status to the bot.
135+
BotStatusManager() StatusManager
125136
}
126137

127138
// LoadComponent is used by the component registration system that

api/discordapi.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818

1919
package api
2020

21+
import (
22+
"fmt"
23+
"github.com/bwmarrin/discordgo"
24+
)
25+
2126
// DiscordGoApiWrapper is a wrapper around some crucial discordgo
2227
// functions. It provides functions that might be
2328
// frequently used without an ongoing event.
@@ -35,6 +40,9 @@ type DiscordGoApiWrapper struct {
3540
type DiscordApiWrapper interface {
3641
// GuildCount returns the number of guilds the bot is currently on.
3742
GuildCount() int
43+
// SetBotStatus updates the status of the bot according to the passed
44+
// SimpleBotStatus data.
45+
SetBotStatus(status SimpleBotStatus) error
3846
}
3947

4048
// DiscordApi is used to obtain the components slash DiscordApiWrapper management
@@ -55,3 +63,28 @@ func (c *Component) DiscordApi() DiscordApiWrapper {
5563
func (dgw *DiscordGoApiWrapper) GuildCount() int {
5664
return len(dgw.owner.discord.State.Guilds)
5765
}
66+
67+
// SimpleBotStatus is a simplified version of discordgo.UpdateStatusData
68+
// that can be used to simply change the status of the bot to something else.
69+
// Note that the URL should be only set for discordgo.ActivityTypeStreaming.
70+
type SimpleBotStatus struct {
71+
ActivityType discordgo.ActivityType
72+
Content string
73+
Url string
74+
}
75+
76+
// SetBotStatus updates the status of the bot according to the passed
77+
// SimpleBotStatus data.
78+
func (dgw *DiscordGoApiWrapper) SetBotStatus(status SimpleBotStatus) error {
79+
switch status.ActivityType {
80+
case discordgo.ActivityTypeGame:
81+
return dgw.owner.discord.UpdateGameStatus(0, status.Content)
82+
case discordgo.ActivityTypeStreaming:
83+
return dgw.owner.discord.UpdateStreamingStatus(0, status.Content, status.Url)
84+
case discordgo.ActivityTypeListening:
85+
return dgw.owner.discord.UpdateListeningStatus(status.Content)
86+
default:
87+
return fmt.Errorf("tried to update bot status to activity type \"%d\", which is not supported",
88+
status.ActivityType)
89+
}
90+
}

api/status.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* JOJO Discord Bot - An advanced multi-purpose discord bot
3+
* Copyright (C) 2022 Lazy Bytez (Elias Knodel, Pascal Zarrad)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package api
20+
21+
import (
22+
"sync"
23+
)
24+
25+
// botStatusManager is the DiscordGoStatusManager instance used across the bots' lifetime.
26+
var botStatusManager *DiscordGoStatusManager
27+
28+
// DiscordGoStatusManager holds the available status of the bot
29+
// and manages the cycling of these.
30+
type DiscordGoStatusManager struct {
31+
mu sync.RWMutex
32+
status []SimpleBotStatus
33+
current int
34+
}
35+
36+
// StatusManager manages the available status
37+
// which are set fopr the bot.
38+
type StatusManager interface {
39+
// AddStatusToRotation adds the given status to the list of
40+
// rotated status.
41+
AddStatusToRotation(status SimpleBotStatus)
42+
// Next works like next on an iterator which self resets automatically.
43+
Next() *SimpleBotStatus
44+
}
45+
46+
// BotStatusManager returns the current StatusManager which
47+
// allows to add additional status to the bot.
48+
func (c *Component) BotStatusManager() StatusManager {
49+
return botStatusManager
50+
}
51+
52+
func init() {
53+
botStatusManager = &DiscordGoStatusManager{
54+
mu: sync.RWMutex{},
55+
status: make([]SimpleBotStatus, 0),
56+
current: 0,
57+
}
58+
}
59+
60+
// AddStatusToRotation adds the given status to the list of
61+
// rotated status.
62+
func (dgsm *DiscordGoStatusManager) AddStatusToRotation(status SimpleBotStatus) {
63+
dgsm.mu.Lock()
64+
defer dgsm.mu.Unlock()
65+
66+
dgsm.status = append(dgsm.status, status)
67+
}
68+
69+
// Next works like next on an iterator which self resets automatically.
70+
func (dgsm *DiscordGoStatusManager) Next() *SimpleBotStatus {
71+
dgsm.mu.Lock()
72+
defer dgsm.mu.Unlock()
73+
74+
statusCount := len(dgsm.status)
75+
if 0 == statusCount {
76+
return nil
77+
}
78+
79+
if dgsm.current >= statusCount {
80+
dgsm.current = 0
81+
}
82+
83+
status := &dgsm.status[dgsm.current]
84+
85+
dgsm.current = dgsm.current + 1
86+
87+
return status
88+
}

api/status_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* JOJO Discord Bot - An advanced multi-purpose discord bot
3+
* Copyright (C) 2022 Lazy Bytez (Elias Knodel, Pascal Zarrad)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package api
20+
21+
import (
22+
"github.com/bwmarrin/discordgo"
23+
"github.com/stretchr/testify/suite"
24+
"sync"
25+
"testing"
26+
)
27+
28+
type StatusManagerSuite struct {
29+
suite.Suite
30+
}
31+
32+
func (suite *StatusManagerSuite) SetupTest() {
33+
botStatusManager = &DiscordGoStatusManager{
34+
mu: sync.RWMutex{},
35+
status: make([]SimpleBotStatus, 0),
36+
current: 0,
37+
}
38+
}
39+
40+
func (suite *StatusManagerSuite) TestNextWithNoStatus() {
41+
for i := 0; i < 10; i++ {
42+
suite.Nil(botStatusManager.Next())
43+
}
44+
}
45+
46+
func (suite *StatusManagerSuite) TestNextWithOneStatus() {
47+
firstStatus := SimpleBotStatus{
48+
ActivityType: discordgo.ActivityTypeGame,
49+
Content: "Test",
50+
}
51+
52+
botStatusManager.AddStatusToRotation(firstStatus)
53+
54+
// Cycle five times
55+
for i := 0; i < 5; i++ {
56+
suite.Equal(firstStatus, *botStatusManager.Next())
57+
}
58+
}
59+
60+
func (suite *StatusManagerSuite) TestNextWithMultipleStatus() {
61+
firstStatus := SimpleBotStatus{
62+
ActivityType: discordgo.ActivityTypeGame,
63+
Content: "Test",
64+
}
65+
66+
secondStatus := SimpleBotStatus{
67+
ActivityType: discordgo.ActivityTypeStreaming,
68+
Url: "https://localhost:8080/",
69+
}
70+
71+
thirdStatus := SimpleBotStatus{
72+
ActivityType: discordgo.ActivityTypeListening,
73+
Content: "Roundabout",
74+
}
75+
76+
botStatusManager.AddStatusToRotation(firstStatus)
77+
botStatusManager.AddStatusToRotation(secondStatus)
78+
botStatusManager.AddStatusToRotation(thirdStatus)
79+
80+
// First cycle
81+
suite.Equal(firstStatus, *botStatusManager.Next())
82+
suite.Equal(secondStatus, *botStatusManager.Next())
83+
suite.Equal(thirdStatus, *botStatusManager.Next())
84+
// Second cycle
85+
suite.Equal(firstStatus, *botStatusManager.Next())
86+
suite.Equal(secondStatus, *botStatusManager.Next())
87+
suite.Equal(thirdStatus, *botStatusManager.Next())
88+
// Third cycle
89+
suite.Equal(firstStatus, *botStatusManager.Next())
90+
suite.Equal(secondStatus, *botStatusManager.Next())
91+
suite.Equal(thirdStatus, *botStatusManager.Next())
92+
// Fourth cycle
93+
suite.Equal(firstStatus, *botStatusManager.Next())
94+
suite.Equal(secondStatus, *botStatusManager.Next())
95+
suite.Equal(thirdStatus, *botStatusManager.Next())
96+
}
97+
98+
func TestStatusManager(t *testing.T) {
99+
suite.Run(t, new(StatusManagerSuite))
100+
}

components/dice/register_command.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,16 @@ func LoadComponent(_ *discordgo.Session) error {
2828
// Register the messageCreate func as a callback for MessageCreate events.
2929
_ = C.SlashCommandManager().Register(diceCommand)
3030

31+
registerBotStatus()
32+
3133
return nil
3234
}
35+
36+
// registerBotStatus registers the bot status for status rotation
37+
// provided by the component.
38+
func registerBotStatus() {
39+
C.BotStatusManager().AddStatusToRotation(api.SimpleBotStatus{
40+
ActivityType: discordgo.ActivityTypeGame,
41+
Content: "/dice | throw dices",
42+
})
43+
}

components/statistics/statistics.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ func LoadComponent(_ *discordgo.Session) error {
4545
_ = C.SlashCommandManager().Register(statsCommand)
4646
_ = C.SlashCommandManager().Register(infoCommand)
4747

48+
registerBotStatus()
4849
registerRoutes()
4950

5051
return nil
5152
}
53+
54+
// registerBotStatus registers the bot status for status rotation
55+
// provided by the component.
56+
func registerBotStatus() {
57+
C.BotStatusManager().AddStatusToRotation(api.SimpleBotStatus{
58+
ActivityType: discordgo.ActivityTypeGame,
59+
Content: "/stats | bot insights",
60+
})
61+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* JOJO Discord Bot - An advanced multi-purpose discord bot
3+
* Copyright (C) 2022 Lazy Bytez (Elias Knodel, Pascal Zarrad)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package bot_status
20+
21+
import (
22+
"github.com/bwmarrin/discordgo"
23+
"github.com/lazybytez/jojo-discord-bot/api"
24+
"time"
25+
)
26+
27+
// BotStatusRotationTime is the time between the status
28+
// that are registered in the DiscordGoStatusManager.
29+
const BotStatusRotationTime = 5 * time.Minute
30+
31+
var C = api.Component{
32+
// Metadata
33+
Code: "bot_status",
34+
Name: "Bot Status",
35+
Description: "This component handles automated rotation and setting of the bot status in Discord.",
36+
LoadPriority: -1000, // Be the last core component, so others can register initial status
37+
38+
State: &api.State{
39+
DefaultEnabled: true,
40+
},
41+
}
42+
43+
// botStatusRotationTicker id the ticker used to periodically
44+
// change the bots' status.
45+
var botStatusRotationTicker *time.Ticker
46+
47+
func init() {
48+
api.RegisterComponent(&C, LoadComponent)
49+
}
50+
51+
// LoadComponent loads the bot core component
52+
// and handles migration of core entities
53+
// and registration of important core event handlers.
54+
func LoadComponent(_ *discordgo.Session) error {
55+
C.HandlerManager().RegisterOnce("start_status_rotation", onBotReady)
56+
57+
return nil
58+
}
59+
60+
// onBotReady starts the bot status rotation.
61+
// At this point, discordgo is fully initialized and connected.
62+
func onBotReady(_ *discordgo.Session, _ *discordgo.Ready) {
63+
startBotStatusRotation()
64+
}
65+
66+
// startBotStatusRotation starts a routine that handles the automated rotation
67+
// of the bots' status.
68+
func startBotStatusRotation() {
69+
botStatusRotationTicker = time.NewTicker(BotStatusRotationTime)
70+
71+
// Initial status rotation
72+
rotateStatus()
73+
74+
// Continues status rotation
75+
go func() {
76+
for range botStatusRotationTicker.C {
77+
rotateStatus()
78+
}
79+
}()
80+
}
81+
82+
// rotateStatus updates the status of the bot by rotating it.
83+
func rotateStatus() {
84+
status := C.BotStatusManager().Next()
85+
86+
if nil == status {
87+
C.Logger().Info("Not updating status, as no status are registered!")
88+
89+
return
90+
}
91+
92+
err := C.DiscordApi().SetBotStatus(*status)
93+
94+
if nil != err {
95+
C.Logger().Err(err, "Could not update the status of the bot due to an unexpected error!")
96+
97+
return
98+
}
99+
100+
C.Logger().Info("Updated bot status to content \"%s\" and url \"%s\" with activity type \"%d\"",
101+
status.Content,
102+
status.Url,
103+
status.ActivityType)
104+
}

0 commit comments

Comments
 (0)