diff --git a/BirthdayBot/BackgroundServiceRunner.vb b/BirthdayBot/BackgroundServiceRunner.vb
new file mode 100644
index 0000000..0d8c5d8
--- /dev/null
+++ b/BirthdayBot/BackgroundServiceRunner.vb
@@ -0,0 +1,58 @@
+Imports System.Threading
+
+'''
+''' Handles the execution of periodic background tasks.
+'''
+Class BackgroundServiceRunner
+ Const Interval = 45 ' Tick interval in seconds. Adjust as needed.
+
+ Private ReadOnly Property Workers As List(Of BackgroundService)
+ Private ReadOnly Property WorkerCancel As New CancellationTokenSource
+
+ Private _workerTask As Task
+ Private _tickCount As Integer
+
+ Sub New(instance As BirthdayBot)
+ Workers = New List(Of BackgroundService) From {
+ {New GuildStatistics(instance)},
+ {New Heartbeat(instance)},
+ {New BirthdayRoleUpdate(instance)}
+ }
+ End Sub
+
+ Public Sub Start()
+ _tickCount = 0
+ _workerTask = Task.Factory.StartNew(AddressOf WorkerLoop, WorkerCancel.Token,
+ TaskCreationOptions.LongRunning, TaskScheduler.Default)
+ End Sub
+
+ Public Async Function Cancel() As Task
+ WorkerCancel.Cancel()
+ Await _workerTask
+ End Function
+
+ '''
+ ''' *The* background task. Executes service tasks and handles errors.
+ '''
+ Private Async Function WorkerLoop() As Task
+ While Not WorkerCancel.IsCancellationRequested
+ Try
+ ' Delay a bit before we start (or continue) work.
+ Await Task.Delay(Interval * 1000, WorkerCancel.Token)
+
+ ' Start background tasks.
+ Dim tasks As New List(Of Task)
+ For Each service In Workers
+ tasks.Add(service.OnTick(_tickCount))
+ Next
+ Await Task.WhenAll(tasks)
+ Catch ex As TaskCanceledException
+ Return
+ Catch ex As Exception
+ Log("Background task", "Unhandled exception in background task thread:")
+ Log("Background task", ex.ToString())
+ End Try
+ _tickCount += 1
+ End While
+ End Function
+End Class
diff --git a/BirthdayBot/BackgroundServices/BackgroundService.vb b/BirthdayBot/BackgroundServices/BackgroundService.vb
new file mode 100644
index 0000000..86ab79f
--- /dev/null
+++ b/BirthdayBot/BackgroundServices/BackgroundService.vb
@@ -0,0 +1,17 @@
+'''
+''' Base class for background services.
+''' Background services are called periodically by another class.
+'''
+MustInherit Class BackgroundService
+ Protected ReadOnly Property BotInstance As BirthdayBot
+
+ Sub New(instance As BirthdayBot)
+ BotInstance = instance
+ End Sub
+
+ Sub Log(message As String)
+ Program.Log(Me.GetType().Name, message)
+ End Sub
+
+ MustOverride Function OnTick(tick As Integer) As Task
+End Class
diff --git a/BirthdayBot/BackgroundWorker.vb b/BirthdayBot/BackgroundServices/BirthdayRoleUpdate.vb
similarity index 59%
rename from BirthdayBot/BackgroundWorker.vb
rename to BirthdayBot/BackgroundServices/BirthdayRoleUpdate.vb
index 0fb653a..3afcd45 100644
--- a/BirthdayBot/BackgroundWorker.vb
+++ b/BirthdayBot/BackgroundServices/BirthdayRoleUpdate.vb
@@ -1,77 +1,40 @@
Imports System.Net
Imports System.Text
-Imports System.Threading
Imports Discord.WebSocket
Imports NodaTime
'''
-''' BirthdayBot's periodic task. Frequently wakes up to take various actions.
+''' Periodically scans all known guilds and adjusts birthday role membership as necessary.
+''' Also handles birthday announcements.
'''
-Class BackgroundWorker
- Private ReadOnly _bot As BirthdayBot
- Private ReadOnly Property WorkerCancel As New CancellationTokenSource
- Private _workerTask As Task
- Const Interval = 45 ' How often the worker wakes up, in seconds. Adjust as needed.
- Private ReadOnly _clock As IClock
+Class BirthdayRoleUpdate
+ Inherits BackgroundService
+ Private ReadOnly Property Clock As IClock
- Sub New(instance As BirthdayBot)
- _bot = instance
- _clock = SystemClock.Instance ' can replace with FakeClock here when testing
+ Public Sub New(instance As BirthdayBot)
+ MyBase.New(instance)
+ Clock = SystemClock.Instance ' can be replaced with FakeClock during testing
End Sub
- Public Sub Start()
- _workerTask = Task.Factory.StartNew(AddressOf WorkerLoop, WorkerCancel.Token,
- TaskCreationOptions.LongRunning, TaskScheduler.Default)
- End Sub
-
- Public Async Function Cancel() As Task
- WorkerCancel.Cancel()
- Await _workerTask
- End Function
-
'''
- ''' Background task. Kicks off many other tasks.
+ ''' Initial processing: Sets up a task per guild and waits on all.
'''
- Private Async Function WorkerLoop() As Task
- While Not WorkerCancel.IsCancellationRequested
- Try
- ' Delay a bit before we start (or continue) work.
- Await Task.Delay(Interval * 1000, WorkerCancel.Token)
-
- ' Start background tasks.
- Dim bgTasks As New List(Of Task) From {
- ReportAsync(),
- BirthdayAsync()
- }
- Await Task.WhenAll(bgTasks)
- Catch ex As TaskCanceledException
- Return
- Catch ex As Exception
- Log("Background task", "Unhandled exception in background task thread:")
- Log("Background task", ex.ToString())
- End Try
- End While
- End Function
-
-#Region "Birthday handling"
- '''
- ''' Birthday tasks processing. Sets up a task per guild and waits on them.
- '''
- Private Async Function BirthdayAsync() As Task
+ Public Overrides Async Function OnTick(tick As Integer) As Task
Dim tasks As New List(Of Task(Of Integer))
- For Each guild In _bot.DiscordClient.Guilds
- Dim t = BirthdayGuildProcessAsync(guild)
+ For Each guild In BotInstance.DiscordClient.Guilds
+ Dim t = ProcessGuildAsync(guild)
tasks.Add(t)
Next
+
Try
Await Task.WhenAll(tasks)
Catch ex As Exception
Dim exs = From task In tasks
Where task.Exception IsNot Nothing
Select task.Exception
- Log("Error", "Encountered one or more unhandled exceptions during birthday processing.")
+ Log($"Encountered {exs.Count} errors during bulk guild processing.")
For Each iex In exs
- Log("Error", iex.ToString())
+ Log(iex.ToString())
Next
End Try
@@ -84,14 +47,10 @@ Class BackgroundWorker
guilds += 1
End If
Next
- If announces > 0 Then Log("Birthday task", $"Announcing {announces} birthday(s) in {guilds} guild(s).")
+ If announces > 0 Then Log($"Announcing {announces} birthday(s) in {guilds} guild(s).")
End Function
- '''
- ''' Birthday processing for an individual guild.
- '''
- ''' Number of birthdays announced.
- Private Async Function BirthdayGuildProcessAsync(guild As SocketGuild) As Task(Of Integer)
+ Async Function ProcessGuildAsync(guild As SocketGuild) As Task(Of Integer)
' Gather required information
Dim tz As String
Dim users As IEnumerable(Of GuildUserSettings)
@@ -99,9 +58,9 @@ Class BackgroundWorker
Dim channel As SocketTextChannel = Nothing
Dim announce As (String, String)
Dim announceping As Boolean
- SyncLock _bot.KnownGuilds
- If Not _bot.KnownGuilds.ContainsKey(guild.Id) Then Return 0
- Dim gs = _bot.KnownGuilds(guild.Id)
+ SyncLock BotInstance.KnownGuilds
+ If Not BotInstance.KnownGuilds.ContainsKey(guild.Id) Then Return 0
+ Dim gs = BotInstance.KnownGuilds(guild.Id)
tz = gs.TimeZone
users = gs.Users
announce = gs.AnnounceMessages
@@ -117,15 +76,15 @@ Class BackgroundWorker
End SyncLock
' Determine who's currently having a birthday
- Dim birthdays = BirthdayCalculate(users, tz)
+ Dim birthdays = GetGuildCurrentBirthdays(users, tz)
' Note: Don't quit here if zero people are having birthdays. Roles may still need to be removed by BirthdayApply.
' Set birthday roles, get list of users that had the role added
' But first check if we are able to do so. Letting all requests fail instead will lead to rate limiting.
Dim announceNames As IEnumerable(Of SocketGuildUser)
- If BirthdayHasGoodRolePermissions(guild, role) Then
+ If HasCorrectRolePermissions(guild, role) Then
Try
- announceNames = Await BirthdayApplyAsync(guild, role, birthdays)
+ announceNames = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
Catch ex As Discord.Net.HttpException
If ex.HttpCode = HttpStatusCode.Forbidden Then
announceNames = Nothing
@@ -138,16 +97,16 @@ Class BackgroundWorker
End If
If announceNames Is Nothing Then
- SyncLock _bot.KnownGuilds
+ SyncLock BotInstance.KnownGuilds
' Nothing on announceNAmes signals failure to apply roles. Set the warning message.
- _bot.KnownGuilds(guild.Id).RoleWarning = True
+ BotInstance.KnownGuilds(guild.Id).RoleWarning = True
End SyncLock
Return 0
End If
If announceNames.Count <> 0 Then
' Send out announcement message
- Await BirthdayAnnounceAsync(announce, announceping, channel, announceNames)
+ Await AnnounceBirthdaysAsync(announce, announceping, channel, announceNames)
End If
Return announceNames.Count
End Function
@@ -155,7 +114,7 @@ Class BackgroundWorker
'''
''' Checks if the bot may be allowed to alter roles.
'''
- Private Function BirthdayHasGoodRolePermissions(guild As SocketGuild, role As SocketRole) As Boolean
+ Private Function HasCorrectRolePermissions(guild As SocketGuild, role As SocketRole) As Boolean
If Not guild.CurrentUser.GuildPermissions.ManageRoles Then
' Bot user cannot manage roles
Return False
@@ -174,7 +133,8 @@ Class BackgroundWorker
''' Gets all known users from the given guild and returns a list including only those who are
''' currently experiencing a birthday in the respective time zone.
'''
- Private Function BirthdayCalculate(guildUsers As IEnumerable(Of GuildUserSettings), defaultTzStr As String) As HashSet(Of ULong)
+ Private Function GetGuildCurrentBirthdays(guildUsers As IEnumerable(Of GuildUserSettings),
+ defaultTzStr As String) As HashSet(Of ULong)
Dim birthdayUsers As New HashSet(Of ULong)
Dim defaultTz As DateTimeZone = Nothing
@@ -196,7 +156,7 @@ Class BackgroundWorker
Dim targetMonth = item.BirthMonth
Dim targetDay = item.BirthDay
- Dim checkNow = _clock.GetCurrentInstant().InZone(tz)
+ Dim checkNow = Clock.GetCurrentInstant().InZone(tz)
' Special case: If birthday is February 29 and it's not a leap year, recognize it on March 1st
If targetMonth = 2 And targetDay = 29 And Not Date.IsLeapYear(checkNow.Year) Then
targetMonth = 3
@@ -214,7 +174,7 @@ Class BackgroundWorker
''' Sets the birthday role to all applicable users. Unsets it from all others who may have it.
'''
''' A list of users who had the birthday role applied. Use for the announcement message.
- Private Async Function BirthdayApplyAsync(g As SocketGuild,
+ Private Async Function UpdateGuildBirthdayRoles(g As SocketGuild,
r As SocketRole,
names As HashSet(Of ULong)) As Task(Of IEnumerable(Of SocketGuildUser))
' Check members currently with the role. Figure out which users to remove it from.
@@ -255,7 +215,7 @@ Class BackgroundWorker
''' Makes (or attempts to make) an announcement in the specified channel that includes all users
''' who have just had their birthday role added.
'''
- Private Async Function BirthdayAnnounceAsync(announce As (String, String),
+ Private Async Function AnnounceBirthdaysAsync(announce As (String, String),
announcePing As Boolean,
c As SocketTextChannel,
names As IEnumerable(Of SocketGuildUser)) As Task
@@ -294,56 +254,4 @@ Class BackgroundWorker
' TODO keep tabs on this somehow for troubleshooting purposes
End Try
End Function
-#End Region
-
-#Region "Activity reporting"
- '''
- ''' Increasing value for regulating how often certain tasks are done.
- ''' For anything relying on this value, also be mindful of the interval value.
- '''
- Private _reportTick As Integer = 0
-
- '''
- ''' Handles various periodic reporting tasks.
- '''
- Private Async Function ReportAsync() As Task
- ReportHeartbeat(_reportTick)
- Await ReportGuildCount(_reportTick)
-
- _reportTick += 1
- End Function
-
- Private Sub ReportHeartbeat(tick As Integer)
- ' Roughly every 15 minutes (interval: 45)
- If tick Mod 20 = 0 Then
- Log("Background task", $"Still alive! Tick: {_reportTick}.")
- End If
- End Sub
-
- Private Async Function ReportGuildCount(tick As Integer) As Task
- ' Roughly every 5 hours (interval: 45)
- If tick Mod 400 <> 2 Then Return
-
- Dim count = _bot.DiscordClient.Guilds.Count
- Log("Report", $"Currently in {count} guild(s).")
-
- Dim dtok = _bot.Config.DBotsToken
- If dtok IsNot Nothing Then
- Const dUrl As String = "https://discord.bots.gg/api/v1/bots/{0}/stats"
-
- Using client As New WebClient()
- Dim uri = New Uri(String.Format(dUrl, CType(_bot.DiscordClient.CurrentUser.Id, String)))
- Dim data = "{ ""guildCount"": " + CType(count, String) + " }"
- client.Headers(HttpRequestHeader.Authorization) = dtok
- client.Headers(HttpRequestHeader.ContentType) = "application/json"
- Try
- Await client.UploadStringTaskAsync(uri, data)
- Log("Server Count", "Count sent to Discord Bots.")
- Catch ex As WebException
- Log("Server Count", "Encountered error on sending to Discord Bots: " + ex.Message)
- End Try
- End Using
- End If
- End Function
-#End Region
End Class
diff --git a/BirthdayBot/BackgroundServices/GuildStatistics.vb b/BirthdayBot/BackgroundServices/GuildStatistics.vb
new file mode 100644
index 0000000..bd55f3d
--- /dev/null
+++ b/BirthdayBot/BackgroundServices/GuildStatistics.vb
@@ -0,0 +1,47 @@
+Imports System.Net
+
+Class GuildStatistics
+ Inherits BackgroundService
+
+ Private ReadOnly Property DBotsToken As String
+
+ Public Sub New(instance As BirthdayBot)
+ MyBase.New(instance)
+ DBotsToken = instance.Config.DBotsToken
+ End Sub
+
+ Public Overrides Async Function OnTick(tick As Integer) As Task
+ ' Activate roughly every 5 hours (interval: 45)
+ If tick Mod 400 <> 2 Then Return
+
+ Dim count = BotInstance.DiscordClient.Guilds.Count
+ Log($"Currently in {count} guild(s).")
+
+ Await SendExternalStatistics(count)
+ End Function
+
+ '''
+ ''' Send statistical information to external services.
+ '''
+ '''
+ ''' Only Discord Bots is currently supported. No plans to support others any time soon.
+ '''
+ Async Function SendExternalStatistics(guildCount As Integer) As Task
+ Dim rptToken = BotInstance.Config.DBotsToken
+ If rptToken Is Nothing Then Return
+
+ Const apiUrl As String = "https://discord.bots.gg/api/v1/bots/{0}/stats"
+ Using client As New WebClient()
+ Dim uri = New Uri(String.Format(apiUrl, CType(BotInstance.DiscordClient.CurrentUser.Id, String)))
+ Dim data = "{ ""guildCount"": " + CType(guildCount, String) + " }"
+ client.Headers(HttpRequestHeader.Authorization) = rptToken
+ client.Headers(HttpRequestHeader.ContentType) = "application/json"
+ Try
+ Await client.UploadStringTaskAsync(uri, data)
+ Log("Discord Bots: Report sent successfully.")
+ Catch ex As WebException
+ Log("Discord Bots: Encountered an error. " + ex.Message)
+ End Try
+ End Using
+ End Function
+End Class
diff --git a/BirthdayBot/BackgroundServices/Heartbeat.vb b/BirthdayBot/BackgroundServices/Heartbeat.vb
new file mode 100644
index 0000000..5b57438
--- /dev/null
+++ b/BirthdayBot/BackgroundServices/Heartbeat.vb
@@ -0,0 +1,20 @@
+'''
+''' Basic heartbeat function - indicates that the background task is still functioning.
+'''
+Class Heartbeat
+ Inherits BackgroundService
+
+ Public Sub New(instance As BirthdayBot)
+ MyBase.New(instance)
+ End Sub
+
+ Public Overrides Function OnTick(tick As Integer) As Task
+ ' Print a message roughly every 15 minutes (assuming 45s per tick).
+ If tick Mod 20 = 0 Then
+ Dim uptime = DateTimeOffset.UtcNow - Program.BotStartTime
+ Log($"Tick {tick:00000} - Bot uptime: {BotUptime()}")
+ End If
+
+ Return Task.CompletedTask
+ End Function
+End Class
diff --git a/BirthdayBot/BirthdayBot.vb b/BirthdayBot/BirthdayBot.vb
index ed1ae96..a6a45aa 100644
--- a/BirthdayBot/BirthdayBot.vb
+++ b/BirthdayBot/BirthdayBot.vb
@@ -15,7 +15,7 @@ Class BirthdayBot
Private ReadOnly _cmdsMods As ManagerCommands
Private WithEvents _client As DiscordSocketClient
- Private ReadOnly _worker As BackgroundWorker
+ Private ReadOnly _worker As BackgroundServiceRunner
Friend ReadOnly Property Config As Configuration
@@ -33,7 +33,7 @@ Class BirthdayBot
_client = dc
KnownGuilds = New Dictionary(Of ULong, GuildSettings)
- _worker = New BackgroundWorker(Me)
+ _worker = New BackgroundServiceRunner(Me)
' Command dispatch set-up
_dispatchCommands = New Dictionary(Of String, CommandHandler)(StringComparer.InvariantCultureIgnoreCase)
diff --git a/BirthdayBot/BirthdayBot.vbproj b/BirthdayBot/BirthdayBot.vbproj
index ee811d8..37cdd6a 100644
--- a/BirthdayBot/BirthdayBot.vbproj
+++ b/BirthdayBot/BirthdayBot.vbproj
@@ -4,8 +4,8 @@
Exe
BirthdayBot
netcoreapp2.0
- 1.0.5
- 1.0.4.0
+ 1.1.0
+ 1.1.0.0
Noiiko
Discord bot for birthday reminders.
diff --git a/BirthdayBot/Common.vb b/BirthdayBot/Common.vb
index dc1bdac..b6a63f2 100644
--- a/BirthdayBot/Common.vb
+++ b/BirthdayBot/Common.vb
@@ -30,4 +30,8 @@ Module Common
{1, "Jan"}, {2, "Feb"}, {3, "Mar"}, {4, "Apr"}, {5, "May"}, {6, "Jun"},
{7, "Jul"}, {8, "Aug"}, {9, "Sep"}, {10, "Oct"}, {11, "Nov"}, {12, "Dec"}
}
+
+ Public Function BotUptime() As String
+ BotUptime = (DateTimeOffset.UtcNow - BotStartTime).ToString("d' days, 'hh':'mm':'ss")
+ End Function
End Module
diff --git a/BirthdayBot/Program.vb b/BirthdayBot/Program.vb
index 9e4c98b..60c341d 100644
--- a/BirthdayBot/Program.vb
+++ b/BirthdayBot/Program.vb
@@ -14,7 +14,9 @@ Module Program
End Set
End Property
- Sub Main(args As String())
+ Sub Main()
+ Log("Birthday Bot", $"Version {System.Reflection.Assembly.GetExecutingAssembly().GetName.Version.ToString(3)} is starting.")
+
BotStartTime = DateTimeOffset.UtcNow
Dim cfg As New Configuration()
diff --git a/BirthdayBot/UserInterface/HelpInfoCommands.vb b/BirthdayBot/UserInterface/HelpInfoCommands.vb
index 60ad3b5..b3b7304 100644
--- a/BirthdayBot/UserInterface/HelpInfoCommands.vb
+++ b/BirthdayBot/UserInterface/HelpInfoCommands.vb
@@ -136,7 +136,7 @@ Friend Class HelpInfoCommands
})
embed.AddField(New EmbedFieldBuilder() With {
.Name = "Examples",
- .Value = String.Format(msghelp2, BackgroundWorker.DefaultAnnounce, BackgroundWorker.DefaultAnnouncePl)
+ .Value = String.Format(msghelp2, BirthdayRoleUpdate.DefaultAnnounce, BirthdayRoleUpdate.DefaultAnnouncePl)
})
Await reqChannel.SendMessageAsync(embed:=embed.Build())
End Function
@@ -145,10 +145,9 @@ Friend Class HelpInfoCommands
' Bot status field
Dim strStatus As New StringBuilder
Dim asmnm = Reflection.Assembly.GetExecutingAssembly.GetName()
- strStatus.AppendLine("Birthday Bot version " + asmnm.Version.ToString(3))
+ strStatus.AppendLine("Birthday Bot v" + asmnm.Version.ToString(3))
strStatus.AppendLine("Server count: " + _discordClient.Guilds.Count.ToString())
- strStatus.AppendLine("Uptime: " + (DateTimeOffset.UtcNow - Program.BotStartTime).ToString("d' days, 'hh':'mm':'ss"))
- strStatus.Append("More info will be shown here soon.")
+ strStatus.AppendLine("Uptime: " + BotUptime())
' TODO fun stats
' current birthdays, total names registered, unique time zones