Skip to content

Commit

Permalink
Merge pull request #125 from lazybytez/feature/bot-status
Browse files Browse the repository at this point in the history
Add bot status management
  • Loading branch information
elias-knodel authored Dec 19, 2022
2 parents cbef507 + 532ba4f commit 3844752
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 0 deletions.
11 changes: 11 additions & 0 deletions api/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ type ServiceManager interface {
// Prefer using the EntityManager instead, as DatabaseAccess is considered
// a low-level api.
DatabaseAccess() *EntityManager
// BotAuditLogger returns the bot audit logger for the current component,
// which allows to create audit log entries.
BotAuditLogger() *BotAuditLogger
// DiscordApi is used to obtain the components slash DiscordApiWrapper management
//
// On first call, this function initializes the private Component.discordAPi
// field. On consecutive calls, the already present DiscordGoApiWrapper will be used.
DiscordApi() DiscordApiWrapper
// BotStatusManager returns the current StatusManager which
// allows to add additional status to the bot.
BotStatusManager() StatusManager
}

// LoadComponent is used by the component registration system that
Expand Down
33 changes: 33 additions & 0 deletions api/discordapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

package api

import (
"fmt"
"github.com/bwmarrin/discordgo"
)

// DiscordGoApiWrapper is a wrapper around some crucial discordgo
// functions. It provides functions that might be
// frequently used without an ongoing event.
Expand All @@ -35,6 +40,9 @@ type DiscordGoApiWrapper struct {
type DiscordApiWrapper interface {
// GuildCount returns the number of guilds the bot is currently on.
GuildCount() int
// SetBotStatus updates the status of the bot according to the passed
// SimpleBotStatus data.
SetBotStatus(status SimpleBotStatus) error
}

// DiscordApi is used to obtain the components slash DiscordApiWrapper management
Expand All @@ -55,3 +63,28 @@ func (c *Component) DiscordApi() DiscordApiWrapper {
func (dgw *DiscordGoApiWrapper) GuildCount() int {
return len(dgw.owner.discord.State.Guilds)
}

// SimpleBotStatus is a simplified version of discordgo.UpdateStatusData
// that can be used to simply change the status of the bot to something else.
// Note that the URL should be only set for discordgo.ActivityTypeStreaming.
type SimpleBotStatus struct {
ActivityType discordgo.ActivityType
Content string
Url string
}

// SetBotStatus updates the status of the bot according to the passed
// SimpleBotStatus data.
func (dgw *DiscordGoApiWrapper) SetBotStatus(status SimpleBotStatus) error {
switch status.ActivityType {
case discordgo.ActivityTypeGame:
return dgw.owner.discord.UpdateGameStatus(0, status.Content)
case discordgo.ActivityTypeStreaming:
return dgw.owner.discord.UpdateStreamingStatus(0, status.Content, status.Url)
case discordgo.ActivityTypeListening:
return dgw.owner.discord.UpdateListeningStatus(status.Content)
default:
return fmt.Errorf("tried to update bot status to activity type \"%d\", which is not supported",
status.ActivityType)
}
}
88 changes: 88 additions & 0 deletions api/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* JOJO Discord Bot - An advanced multi-purpose discord bot
* Copyright (C) 2022 Lazy Bytez (Elias Knodel, Pascal Zarrad)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package api

import (
"sync"
)

// botStatusManager is the DiscordGoStatusManager instance used across the bots' lifetime.
var botStatusManager *DiscordGoStatusManager

// DiscordGoStatusManager holds the available status of the bot
// and manages the cycling of these.
type DiscordGoStatusManager struct {
mu sync.RWMutex
status []SimpleBotStatus
current int
}

// StatusManager manages the available status
// which are set fopr the bot.
type StatusManager interface {
// AddStatusToRotation adds the given status to the list of
// rotated status.
AddStatusToRotation(status SimpleBotStatus)
// Next works like next on an iterator which self resets automatically.
Next() *SimpleBotStatus
}

// BotStatusManager returns the current StatusManager which
// allows to add additional status to the bot.
func (c *Component) BotStatusManager() StatusManager {
return botStatusManager
}

func init() {
botStatusManager = &DiscordGoStatusManager{
mu: sync.RWMutex{},
status: make([]SimpleBotStatus, 0),
current: 0,
}
}

// AddStatusToRotation adds the given status to the list of
// rotated status.
func (dgsm *DiscordGoStatusManager) AddStatusToRotation(status SimpleBotStatus) {
dgsm.mu.Lock()
defer dgsm.mu.Unlock()

dgsm.status = append(dgsm.status, status)
}

// Next works like next on an iterator which self resets automatically.
func (dgsm *DiscordGoStatusManager) Next() *SimpleBotStatus {
dgsm.mu.Lock()
defer dgsm.mu.Unlock()

statusCount := len(dgsm.status)
if 0 == statusCount {
return nil
}

if dgsm.current >= statusCount {
dgsm.current = 0
}

status := &dgsm.status[dgsm.current]

dgsm.current = dgsm.current + 1

return status
}
100 changes: 100 additions & 0 deletions api/status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* JOJO Discord Bot - An advanced multi-purpose discord bot
* Copyright (C) 2022 Lazy Bytez (Elias Knodel, Pascal Zarrad)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package api

import (
"github.com/bwmarrin/discordgo"
"github.com/stretchr/testify/suite"
"sync"
"testing"
)

type StatusManagerSuite struct {
suite.Suite
}

func (suite *StatusManagerSuite) SetupTest() {
botStatusManager = &DiscordGoStatusManager{
mu: sync.RWMutex{},
status: make([]SimpleBotStatus, 0),
current: 0,
}
}

func (suite *StatusManagerSuite) TestNextWithNoStatus() {
for i := 0; i < 10; i++ {
suite.Nil(botStatusManager.Next())
}
}

func (suite *StatusManagerSuite) TestNextWithOneStatus() {
firstStatus := SimpleBotStatus{
ActivityType: discordgo.ActivityTypeGame,
Content: "Test",
}

botStatusManager.AddStatusToRotation(firstStatus)

// Cycle five times
for i := 0; i < 5; i++ {
suite.Equal(firstStatus, *botStatusManager.Next())
}
}

func (suite *StatusManagerSuite) TestNextWithMultipleStatus() {
firstStatus := SimpleBotStatus{
ActivityType: discordgo.ActivityTypeGame,
Content: "Test",
}

secondStatus := SimpleBotStatus{
ActivityType: discordgo.ActivityTypeStreaming,
Url: "https://localhost:8080/",
}

thirdStatus := SimpleBotStatus{
ActivityType: discordgo.ActivityTypeListening,
Content: "Roundabout",
}

botStatusManager.AddStatusToRotation(firstStatus)
botStatusManager.AddStatusToRotation(secondStatus)
botStatusManager.AddStatusToRotation(thirdStatus)

// First cycle
suite.Equal(firstStatus, *botStatusManager.Next())
suite.Equal(secondStatus, *botStatusManager.Next())
suite.Equal(thirdStatus, *botStatusManager.Next())
// Second cycle
suite.Equal(firstStatus, *botStatusManager.Next())
suite.Equal(secondStatus, *botStatusManager.Next())
suite.Equal(thirdStatus, *botStatusManager.Next())
// Third cycle
suite.Equal(firstStatus, *botStatusManager.Next())
suite.Equal(secondStatus, *botStatusManager.Next())
suite.Equal(thirdStatus, *botStatusManager.Next())
// Fourth cycle
suite.Equal(firstStatus, *botStatusManager.Next())
suite.Equal(secondStatus, *botStatusManager.Next())
suite.Equal(thirdStatus, *botStatusManager.Next())
}

func TestStatusManager(t *testing.T) {
suite.Run(t, new(StatusManagerSuite))
}
11 changes: 11 additions & 0 deletions components/dice/register_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,16 @@ func LoadComponent(_ *discordgo.Session) error {
// Register the messageCreate func as a callback for MessageCreate events.
_ = C.SlashCommandManager().Register(diceCommand)

registerBotStatus()

return nil
}

// registerBotStatus registers the bot status for status rotation
// provided by the component.
func registerBotStatus() {
C.BotStatusManager().AddStatusToRotation(api.SimpleBotStatus{
ActivityType: discordgo.ActivityTypeGame,
Content: "/dice | throw dices",
})
}
10 changes: 10 additions & 0 deletions components/statistics/statistics.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ func LoadComponent(_ *discordgo.Session) error {
_ = C.SlashCommandManager().Register(statsCommand)
_ = C.SlashCommandManager().Register(infoCommand)

registerBotStatus()
registerRoutes()

return nil
}

// registerBotStatus registers the bot status for status rotation
// provided by the component.
func registerBotStatus() {
C.BotStatusManager().AddStatusToRotation(api.SimpleBotStatus{
ActivityType: discordgo.ActivityTypeGame,
Content: "/stats | bot insights",
})
}
104 changes: 104 additions & 0 deletions core_components/bot_status/bot_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* JOJO Discord Bot - An advanced multi-purpose discord bot
* Copyright (C) 2022 Lazy Bytez (Elias Knodel, Pascal Zarrad)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package bot_status

import (
"github.com/bwmarrin/discordgo"
"github.com/lazybytez/jojo-discord-bot/api"
"time"
)

// BotStatusRotationTime is the time between the status
// that are registered in the DiscordGoStatusManager.
const BotStatusRotationTime = 5 * time.Minute

var C = api.Component{
// Metadata
Code: "bot_status",
Name: "Bot Status",
Description: "This component handles automated rotation and setting of the bot status in Discord.",
LoadPriority: -1000, // Be the last core component, so others can register initial status

State: &api.State{
DefaultEnabled: true,
},
}

// botStatusRotationTicker id the ticker used to periodically
// change the bots' status.
var botStatusRotationTicker *time.Ticker

func init() {
api.RegisterComponent(&C, LoadComponent)
}

// LoadComponent loads the bot core component
// and handles migration of core entities
// and registration of important core event handlers.
func LoadComponent(_ *discordgo.Session) error {
C.HandlerManager().RegisterOnce("start_status_rotation", onBotReady)

return nil
}

// onBotReady starts the bot status rotation.
// At this point, discordgo is fully initialized and connected.
func onBotReady(_ *discordgo.Session, _ *discordgo.Ready) {
startBotStatusRotation()
}

// startBotStatusRotation starts a routine that handles the automated rotation
// of the bots' status.
func startBotStatusRotation() {
botStatusRotationTicker = time.NewTicker(BotStatusRotationTime)

// Initial status rotation
rotateStatus()

// Continues status rotation
go func() {
for range botStatusRotationTicker.C {
rotateStatus()
}
}()
}

// rotateStatus updates the status of the bot by rotating it.
func rotateStatus() {
status := C.BotStatusManager().Next()

if nil == status {
C.Logger().Info("Not updating status, as no status are registered!")

return
}

err := C.DiscordApi().SetBotStatus(*status)

if nil != err {
C.Logger().Err(err, "Could not update the status of the bot due to an unexpected error!")

return
}

C.Logger().Info("Updated bot status to content \"%s\" and url \"%s\" with activity type \"%d\"",
status.Content,
status.Url,
status.ActivityType)
}
Loading

0 comments on commit 3844752

Please sign in to comment.