mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-21 21:54:36 +00:00
Split BackgroundWorker into various components
This commit is contained in:
parent
9608a29a8a
commit
fe5bdb53f9
10 changed files with 188 additions and 133 deletions
58
BirthdayBot/BackgroundServiceRunner.vb
Normal file
58
BirthdayBot/BackgroundServiceRunner.vb
Normal file
|
@ -0,0 +1,58 @@
|
|||
Imports System.Threading
|
||||
|
||||
''' <summary>
|
||||
''' Handles the execution of periodic background tasks.
|
||||
''' </summary>
|
||||
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
|
||||
|
||||
''' <summary>
|
||||
''' *The* background task. Executes service tasks and handles errors.
|
||||
''' </summary>
|
||||
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
|
17
BirthdayBot/BackgroundServices/BackgroundService.vb
Normal file
17
BirthdayBot/BackgroundServices/BackgroundService.vb
Normal file
|
@ -0,0 +1,17 @@
|
|||
''' <summary>
|
||||
''' Base class for background services.
|
||||
''' Background services are called periodically by another class.
|
||||
''' </summary>
|
||||
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
|
|
@ -1,77 +1,40 @@
|
|||
Imports System.Net
|
||||
Imports System.Text
|
||||
Imports System.Threading
|
||||
Imports Discord.WebSocket
|
||||
Imports NodaTime
|
||||
|
||||
''' <summary>
|
||||
''' 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.
|
||||
''' </summary>
|
||||
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
|
||||
|
||||
''' <summary>
|
||||
''' Background task. Kicks off many other tasks.
|
||||
''' Initial processing: Sets up a task per guild and waits on all.
|
||||
''' </summary>
|
||||
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"
|
||||
''' <summary>
|
||||
''' Birthday tasks processing. Sets up a task per guild and waits on them.
|
||||
''' </summary>
|
||||
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
|
||||
|
||||
''' <summary>
|
||||
''' Birthday processing for an individual guild.
|
||||
''' </summary>
|
||||
''' <returns>Number of birthdays announced.</returns>
|
||||
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
|
|||
''' <summary>
|
||||
''' Checks if the bot may be allowed to alter roles.
|
||||
''' </summary>
|
||||
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.
|
||||
''' </summary>
|
||||
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.
|
||||
''' </summary>
|
||||
''' <returns>A list of users who had the birthday role applied. Use for the announcement message.</returns>
|
||||
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.
|
||||
''' </summary>
|
||||
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"
|
||||
''' <summary>
|
||||
''' Increasing value for regulating how often certain tasks are done.
|
||||
''' For anything relying on this value, also be mindful of the interval value.
|
||||
''' </summary>
|
||||
Private _reportTick As Integer = 0
|
||||
|
||||
''' <summary>
|
||||
''' Handles various periodic reporting tasks.
|
||||
''' </summary>
|
||||
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
|
47
BirthdayBot/BackgroundServices/GuildStatistics.vb
Normal file
47
BirthdayBot/BackgroundServices/GuildStatistics.vb
Normal file
|
@ -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
|
||||
|
||||
''' <summary>
|
||||
''' Send statistical information to external services.
|
||||
''' </summary>
|
||||
''' <remarks>
|
||||
''' Only Discord Bots is currently supported. No plans to support others any time soon.
|
||||
''' </remarks>
|
||||
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
|
20
BirthdayBot/BackgroundServices/Heartbeat.vb
Normal file
20
BirthdayBot/BackgroundServices/Heartbeat.vb
Normal file
|
@ -0,0 +1,20 @@
|
|||
''' <summary>
|
||||
''' Basic heartbeat function - indicates that the background task is still functioning.
|
||||
''' </summary>
|
||||
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
|
|
@ -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)
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>BirthdayBot</RootNamespace>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.5</Version>
|
||||
<AssemblyVersion>1.0.4.0</AssemblyVersion>
|
||||
<Version>1.1.0</Version>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<Authors>Noiiko</Authors>
|
||||
<Company />
|
||||
<Description>Discord bot for birthday reminders.</Description>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue