diff --git a/BirthdayBot/BackgroundServices/BirthdayRoleUpdate.vb b/BirthdayBot/BackgroundServices/BirthdayRoleUpdate.vb
index 8d718c4..f262904 100644
--- a/BirthdayBot/BackgroundServices/BirthdayRoleUpdate.vb
+++ b/BirthdayBot/BackgroundServices/BirthdayRoleUpdate.vb
@@ -18,7 +18,7 @@ Class BirthdayRoleUpdate
''' Does processing on all available guilds at once.
'''
Public Overrides Async Function OnTick() As Task
- Dim tasks As New List(Of Task(Of Integer))
+ Dim tasks As New List(Of Task)
For Each guild In BotInstance.DiscordClient.Guilds
Dim t = ProcessGuildAsync(guild)
tasks.Add(t)
@@ -32,6 +32,7 @@ Class BirthdayRoleUpdate
Select task.Exception
Log($"Encountered {exs.Count} errors during bulk guild processing.")
For Each iex In exs
+ ' TODO probably not a good idea
Log(iex.ToString())
Next
End Try
@@ -53,7 +54,10 @@ Class BirthdayRoleUpdate
' TODO metrics for role sets, unsets, announcements - and I mentioned this above too
End Function
- Private Async Function ProcessGuildAsync(guild As SocketGuild) As Task(Of Integer)
+ '''
+ ''' Main function where actual guild processing occurs.
+ '''
+ Private Async Function ProcessGuildAsync(guild As SocketGuild) As Task
' Gather required information
Dim tz As String
Dim users As IEnumerable(Of GuildUserSettings)
@@ -61,15 +65,19 @@ Class BirthdayRoleUpdate
Dim channel As SocketTextChannel = Nothing
Dim announce As (String, String)
Dim announceping As Boolean
+ Dim op As OperationStatus
- If Not BotInstance.GuildCache.ContainsKey(guild.Id) Then Return 0 ' guild not yet fully loaded; skip processing
+ ' Skip processing of guild if local info has not yet been loaded
+ If Not BotInstance.GuildCache.ContainsKey(guild.Id) Then Return
+ ' Lock once to grab all info
Dim gs = BotInstance.GuildCache(guild.Id)
With gs
tz = .TimeZone
users = .Users
announce = .AnnounceMessages
announceping = .AnnouncePing
+ op = .OperationLog
If .AnnounceChannelId.HasValue Then channel = guild.GetTextChannel(gs.AnnounceChannelId.Value)
If .RoleId.HasValue Then role = guild.GetRole(gs.RoleId.Value)
@@ -81,55 +89,57 @@ Class BirthdayRoleUpdate
' 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 correctRoleSettings = HasCorrectRoleSettings(guild, role)
- Dim gotForbidden = False
-
- Dim announceNames As IEnumerable(Of SocketGuildUser) = Nothing
- If correctRoleSettings Then
- Try
- announceNames = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
- Catch ex As Discord.Net.HttpException
- If ex.HttpCode = HttpStatusCode.Forbidden Then
- gotForbidden = True
- Else
- Throw
- End If
- End Try
+ Dim roleCheck = CheckCorrectRoleSettings(guild, role)
+ If Not roleCheck.Item1 Then
+ SyncLock op
+ op(OperationType.BirthdayRole) = New OperationInfo(New Exception(roleCheck.Item2))
+ op(OperationType.BirthdayAnnounce) = Nothing
+ End SyncLock
+ Return
End If
- ' Update warning flag
- Dim updateError = Not correctRoleSettings Or gotForbidden
- ' Quit now if the warning flag was set. Announcement data is not available.
- If updateError Then Return 0
+ Dim announcementList As IEnumerable(Of SocketGuildUser)
+ ' Do actual role updating
+ Try
+ announcementList = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
+ SyncLock op
+ op(OperationType.BirthdayRole) = New OperationInfo()
+ End SyncLock
+ Catch ex As Discord.Net.HttpException
+ SyncLock op
+ op(OperationType.BirthdayRole) = New OperationInfo(ex)
+ op(OperationType.BirthdayAnnounce) = Nothing
+ End SyncLock
+ If ex.HttpCode <> HttpStatusCode.Forbidden Then
+ ' Send unusual exceptions to calling method
+ Throw
+ End If
+ Return
+ End Try
- If announceNames.Count <> 0 Then
- ' Send out announcement message
- Await AnnounceBirthdaysAsync(announce, announceping, channel, announceNames)
+ If announcementList.Count <> 0 Then
+ Await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList, op)
End If
- Return announceNames.Count
End Function
'''
''' Checks if the bot may be allowed to alter roles.
'''
- Private Function HasCorrectRoleSettings(guild As SocketGuild, role As SocketRole) As Boolean
+ Private Function CheckCorrectRoleSettings(guild As SocketGuild, role As SocketRole) As (Boolean, String)
If role Is Nothing Then
- ' Designated role not found or defined in guild
- Return False
+ Return (False, "Designated role not found or defined in guild")
End If
If Not guild.CurrentUser.GuildPermissions.ManageRoles Then
- ' Bot user cannot manage roles
- Return False
+ Return (False, "Bot does not contain Manage Roles permission")
End If
' Check potential role order conflict
If role.Position >= guild.CurrentUser.Hierarchy Then
- ' Target role is at or above bot's highest role.
- Return False
+ Return (False, "Targeted role is at or above bot's highest rank")
End If
- Return True
+ Return (True, "Success")
End Function
'''
@@ -219,10 +229,16 @@ Class BirthdayRoleUpdate
''' who have just had their birthday role added.
'''
Private Async Function AnnounceBirthdaysAsync(announce As (String, String),
- announcePing As Boolean,
- c As SocketTextChannel,
- names As IEnumerable(Of SocketGuildUser)) As Task
- If c Is Nothing Then Return
+ announcePing As Boolean,
+ c As SocketTextChannel,
+ names As IEnumerable(Of SocketGuildUser),
+ op As OperationStatus) As Task
+ If c Is Nothing Then
+ SyncLock op
+ op(OperationType.BirthdayAnnounce) = New OperationInfo("Announcement channel missing or undefined")
+ End SyncLock
+ Return
+ End If
Dim announceMsg As String
If names.Count = 1 Then
@@ -252,9 +268,13 @@ Class BirthdayRoleUpdate
Try
Await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString()))
+ SyncLock op
+ op(OperationType.BirthdayAnnounce) = New OperationInfo()
+ End SyncLock
Catch ex As Discord.Net.HttpException
- ' Ignore
- ' TODO keep tabs on this somehow for troubleshooting purposes
+ SyncLock op
+ op(OperationType.BirthdayAnnounce) = New OperationInfo(ex)
+ End SyncLock
End Try
End Function
End Class
diff --git a/BirthdayBot/BirthdayBot.vb b/BirthdayBot/BirthdayBot.vb
index 330ce79..f7ce32c 100644
--- a/BirthdayBot/BirthdayBot.vb
+++ b/BirthdayBot/BirthdayBot.vb
@@ -10,6 +10,7 @@ Class BirthdayBot
Private ReadOnly _cmdsListing As ListingCommands
Private ReadOnly _cmdsHelp As HelpInfoCommands
Private ReadOnly _cmdsMods As ManagerCommands
+ Private ReadOnly _cmdsDiag As DiagnosticCommands
Private WithEvents Client As DiscordShardedClient
Private ReadOnly _worker As BackgroundServiceRunner
@@ -49,15 +50,21 @@ Class BirthdayBot
For Each item In _cmdsMods.Commands
_dispatchCommands.Add(item.Item1, item.Item2)
Next
+ _cmdsDiag = New DiagnosticCommands(Me, conf)
+ For Each item In _cmdsDiag.Commands
+ _dispatchCommands.Add(item.Item1, item.Item2)
+ Next
End Sub
Public Async Function Start() As Task
Await Client.LoginAsync(TokenType.Bot, Config.BotToken)
Await Client.StartAsync()
+#If Not DEBUG Then
Log("Background processing", "Delaying start")
Await Task.Delay(90000) ' TODO don't keep doing this
Log("Background processing", "Delay complete")
+#End If
_worker.Start()
Await Task.Delay(-1)
@@ -85,8 +92,8 @@ Class BirthdayBot
Return Task.CompletedTask
End Function
- Private Async Function SetStatus() As Task Handles Client.ShardConnected
- Await Client.SetGameAsync(CommandPrefix + "help")
+ Private Async Function SetStatus(shard As DiscordSocketClient) As Task Handles Client.ShardConnected
+ Await shard.SetGameAsync(CommandPrefix + "help")
End Function
Private Async Function Dispatch(msg As SocketMessage) As Task Handles Client.MessageReceived
diff --git a/BirthdayBot/BirthdayBot.vbproj b/BirthdayBot/BirthdayBot.vbproj
index 93b9408..9eef773 100644
--- a/BirthdayBot/BirthdayBot.vbproj
+++ b/BirthdayBot/BirthdayBot.vbproj
@@ -4,7 +4,7 @@
Exe
BirthdayBot
netcoreapp2.0
- 1.3.4
+ 1.3.6
Noi
Discord bot for birthday reminders.
diff --git a/BirthdayBot/Configuration.vb b/BirthdayBot/Configuration.vb
index 60ada1e..6c3920e 100644
--- a/BirthdayBot/Configuration.vb
+++ b/BirthdayBot/Configuration.vb
@@ -7,6 +7,8 @@ Imports System.IO
'''
Class Configuration
Public ReadOnly Property BotToken As String
+ Public ReadOnly Property LogWebhook As String
+ Public ReadOnly Property DiagnosticChannel As ULong
Public ReadOnly Property DBotsToken As String
Public ReadOnly Property DatabaseSettings As Database
@@ -23,11 +25,19 @@ Class Configuration
End If
Dim jc = JObject.Parse(File.ReadAllText(confPath))
- BotToken = jc("BotToken").Value(Of String)()
+
+ BotToken = jc("BotToken")?.Value(Of String)()
If String.IsNullOrWhiteSpace(BotToken) Then
Throw New Exception("'BotToken' must be specified.")
End If
+ LogWebhook = jc("LogWebhook")?.Value(Of String)()
+ If String.IsNullOrWhiteSpace(LogWebhook) Then
+ Throw New Exception("'LogWebhook' must be specified.")
+ End If
+
+ DiagnosticChannel = jc("DiagnosticChannel").Value(Of ULong)()
+
Dim dbj = jc("DBotsToken")
If dbj IsNot Nothing Then
DBotsToken = dbj.Value(Of String)()
@@ -35,7 +45,7 @@ Class Configuration
DBotsToken = Nothing
End If
- Dim sqlcs = jc("SqlConnectionString").Value(Of String)()
+ Dim sqlcs = jc("SqlConnectionString")?.Value(Of String)()
If String.IsNullOrWhiteSpace(sqlcs) Then
Throw New Exception("'SqlConnectionString' must be specified.")
End If
diff --git a/BirthdayBot/Data/GuildStateInformation.vb b/BirthdayBot/Data/GuildStateInformation.vb
index 0b7122e..0d7b56e 100644
--- a/BirthdayBot/Data/GuildStateInformation.vb
+++ b/BirthdayBot/Data/GuildStateInformation.vb
@@ -7,7 +7,7 @@ Imports NpgsqlTypes
''' Holds various pieces of state information for a guild the bot is operating in.
''' Includes, among other things, a copy of the guild's settings and a list of all known users with birthdays.
'''
-Friend Class GuildStateInformation
+Class GuildStateInformation
Public ReadOnly Property GuildId As ULong
Private ReadOnly _db As Database
Private _bdayRole As ULong?
@@ -19,9 +19,10 @@ Friend Class GuildStateInformation
Private _announceMsgPl As String
Private _announcePing As Boolean
Private ReadOnly _userCache As Dictionary(Of ULong, GuildUserSettings)
+ Public ReadOnly Property OperationLog As OperationStatus
'''
- ''' Gets a list of cached users. Use sparingly.
+ ''' Gets a list of cached registered user information.
'''
Public ReadOnly Property Users As IEnumerable(Of GuildUserSettings)
Get
@@ -115,6 +116,9 @@ Friend Class GuildStateInformation
' Called by LoadSettingsAsync. Double-check ordinals when changes are made.
Private Sub New(reader As DbDataReader, dbconfig As Database)
_db = dbconfig
+
+ OperationLog = New OperationStatus()
+
GuildId = CULng(reader.GetInt64(0))
' Weird: if using a ternary operator with a ULong?, Nothing resolves to 0 despite Option Strict On.
If Not reader.IsDBNull(1) Then
diff --git a/BirthdayBot/Diagnostics/DiagnosticCommands.vb b/BirthdayBot/Diagnostics/DiagnosticCommands.vb
new file mode 100644
index 0000000..e03e149
--- /dev/null
+++ b/BirthdayBot/Diagnostics/DiagnosticCommands.vb
@@ -0,0 +1,75 @@
+Imports Discord.WebSocket
+Imports Discord.Webhook
+Imports System.Text
+'''
+''' Implements the command used by global bot moderators to get operation info for each guild.
+'''
+Class DiagnosticCommands
+ Inherits CommandsCommon
+
+ Private ReadOnly _webhook As DiscordWebhookClient
+ Private ReadOnly _diagChannel As ULong
+
+ Sub New(inst As BirthdayBot, db As Configuration)
+ MyBase.New(inst, db)
+
+ _webhook = New DiscordWebhookClient(db.LogWebhook)
+ _diagChannel = db.DiagnosticChannel
+ End Sub
+
+ Public Overrides ReadOnly Property Commands As IEnumerable(Of (String, CommandHandler))
+ Get
+ Return New List(Of (String, CommandHandler)) From {
+ ("diag", AddressOf CmdDiag),
+ ("checkme", AddressOf CmdCheckme)
+ }
+ End Get
+ End Property
+
+ ' Dumps all known guild information to the given webhook
+ Private Async Function CmdDiag(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
+ ' Ignore if not in the correct channel
+ If reqChannel.Id <> _diagChannel Then Return
+
+ ' Requires two parameters: (cmd) (guild id)
+ If param.Length <> 2 Then
+ Await reqChannel.SendMessageAsync(":x: Usage: (command) (guild ID)")
+ Return
+ End If
+
+ Dim rgid As ULong
+ If Not ULong.TryParse(param(1), rgid) Then
+ Await reqChannel.SendMessageAsync(":x: Cannot parse numeric guild ID")
+ Return
+ End If
+
+ Dim guild = Instance.DiscordClient.GetGuild(rgid)
+ If guild Is Nothing Then
+ Await reqChannel.SendMessageAsync(":x: Guild is not known to the bot")
+ End If
+
+ Dim gi = Instance.GuildCache(rgid)
+ If gi Is Nothing Then
+ Await reqChannel.SendMessageAsync(":x: Guild is known, but information is not available.")
+ End If
+
+ Await reqChannel.SendMessageAsync(":white_check_mark: Compiling info and sending to webhook.")
+
+ Dim report As New StringBuilder
+ report.AppendLine("=-=-=-=-GUILD INFORMATION-=-=-=-=")
+ ' TODO dump config info
+ report.AppendLine($"{guild.Id}: {guild.Name}")
+ report.AppendLine($"User count: {guild.Users.Count}")
+ report.AppendLine("---")
+ SyncLock gi.OperationLog
+ report.Append(gi.OperationLog.GetDiagStrings())
+ End SyncLock
+ report.AppendLine("**Note**: Full stack traces for captured exceptions are printed to console.")
+
+ Await _webhook.SendMessageAsync(report.ToString())
+ End Function
+
+ Private Async Function CmdCheckme(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
+ Await _webhook.SendMessageAsync($"{reqUser.Username}#{reqUser.Discriminator}: {reqChannel.Guild.Id} checkme")
+ End Function
+End Class
diff --git a/BirthdayBot/Diagnostics/OperationInfo.vb b/BirthdayBot/Diagnostics/OperationInfo.vb
new file mode 100644
index 0000000..fa25852
--- /dev/null
+++ b/BirthdayBot/Diagnostics/OperationInfo.vb
@@ -0,0 +1,37 @@
+'''
+''' Information regarding a single type of operation.
+'''
+Class OperationInfo
+ '''
+ ''' The time in which the respective operation was attempted.
+ '''
+ ReadOnly Property Timestamp As DateTimeOffset
+ '''
+ ''' Any exception encountered during the respective operation.
+ '''
+ ''' Nothing/null if the previous given operation was a success.
+ ReadOnly Property Exception As Exception
+
+ '''
+ ''' Creates an instance containing a success status.
+ '''
+ Sub New()
+ Timestamp = DateTimeOffset.UtcNow
+ End Sub
+
+ '''
+ ''' Creates an instance containing a captured exception
+ '''
+ Sub New(ex As Exception)
+ Me.New()
+ Exception = ex
+ End Sub
+
+ '''
+ ''' Creates an instance containing a custom error message
+ '''
+ Sub New(message As String)
+ Me.New()
+ Exception = New Exception(message)
+ End Sub
+End Class
\ No newline at end of file
diff --git a/BirthdayBot/Diagnostics/OperationStatus.vb b/BirthdayBot/Diagnostics/OperationStatus.vb
new file mode 100644
index 0000000..53bfc50
--- /dev/null
+++ b/BirthdayBot/Diagnostics/OperationStatus.vb
@@ -0,0 +1,49 @@
+Imports System.Text
+'''
+''' Holds information regarding previous operations done on a guild and their most recent success/error status.
+'''
+Class OperationStatus
+ Private ReadOnly _log As New Dictionary(Of OperationType, OperationInfo)
+
+ Default Public Property Item(otype As OperationType) As OperationInfo
+ Get
+ Dim o As OperationInfo = Nothing
+ If Not _log.TryGetValue(otype, o) Then
+ Return Nothing
+ End If
+ Return o
+ End Get
+ Set(value As OperationInfo)
+ If value Is Nothing Then
+ _log.Remove(otype)
+ Else
+ _log(otype) = value
+ End If
+ End Set
+ End Property
+
+ '''
+ ''' Prepares known information in a displayable format.
+ '''
+ Public Function GetDiagStrings() As String
+ Dim report As New StringBuilder
+ For Each otype As OperationType In [Enum].GetValues(GetType(OperationType))
+ Dim prefix = $"`{[Enum].GetName(GetType(OperationType), otype)}`: "
+
+ Dim info = Item(otype)
+ If info Is Nothing Then
+ report.AppendLine(prefix + "No data")
+ Continue For
+ End If
+ prefix += info.Timestamp.ToString("u") + " "
+
+ If info.Exception Is Nothing Then
+ report.AppendLine(prefix + "Success")
+ Else
+ Log("OperationStatus", prefix + info.Exception.ToString())
+ report.AppendLine(prefix + info.Exception.Message)
+ End If
+ Next
+ Return report.ToString()
+ End Function
+End Class
diff --git a/BirthdayBot/Diagnostics/OperationType.vb b/BirthdayBot/Diagnostics/OperationType.vb
new file mode 100644
index 0000000..b752115
--- /dev/null
+++ b/BirthdayBot/Diagnostics/OperationType.vb
@@ -0,0 +1,5 @@
+Enum OperationType
+ BirthdayRole
+ BirthdayAnnounce
+ CommandDispatch
+End Enum
\ No newline at end of file