Added operation diagnostic data

To assist with troubleshooting
This commit is contained in:
Noikoio 2019-12-11 19:14:03 -08:00
parent b31813c210
commit 3fc13efc57
9 changed files with 253 additions and 46 deletions

View file

@ -18,7 +18,7 @@ Class BirthdayRoleUpdate
''' Does processing on all available guilds at once.
''' </summary>
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)
''' <summary>
''' Main function where actual guild processing occurs.
''' </summary>
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
''' <summary>
''' Checks if the bot may be allowed to alter roles.
''' </summary>
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
''' <summary>
@ -219,10 +229,16 @@ Class BirthdayRoleUpdate
''' who have just had their birthday role added.
''' </summary>
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

View file

@ -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

View file

@ -4,7 +4,7 @@
<OutputType>Exe</OutputType>
<RootNamespace>BirthdayBot</RootNamespace>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.3.4</Version>
<Version>1.3.6</Version>
<Authors>Noi</Authors>
<Company />
<Description>Discord bot for birthday reminders.</Description>

View file

@ -7,6 +7,8 @@ Imports System.IO
''' </summary>
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

View file

@ -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.
''' </summary>
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
''' <summary>
''' Gets a list of cached users. Use sparingly.
''' Gets a list of cached registered user information.
''' </summary>
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

View file

@ -0,0 +1,75 @@
Imports Discord.WebSocket
Imports Discord.Webhook
Imports System.Text
''' <summary>
''' Implements the command used by global bot moderators to get operation info for each guild.
''' </summary>
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

View file

@ -0,0 +1,37 @@
''' <summary>
''' Information regarding a single type of operation.
''' </summary>
Class OperationInfo
''' <summary>
''' The time in which the respective operation was attempted.
''' </summary>
ReadOnly Property Timestamp As DateTimeOffset
''' <summary>
''' Any exception encountered during the respective operation.
''' </summary>
''' <returns>Nothing/null if the previous given operation was a success.</returns>
ReadOnly Property Exception As Exception
''' <summary>
''' Creates an instance containing a success status.
''' </summary>
Sub New()
Timestamp = DateTimeOffset.UtcNow
End Sub
''' <summary>
''' Creates an instance containing a captured exception
''' </summary>
Sub New(ex As Exception)
Me.New()
Exception = ex
End Sub
''' <summary>
''' Creates an instance containing a custom error message
''' </summary>
Sub New(message As String)
Me.New()
Exception = New Exception(message)
End Sub
End Class

View file

@ -0,0 +1,49 @@
Imports System.Text
''' <summary>
''' Holds information regarding previous operations done on a guild and their most recent success/error status.
''' </summary>
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
''' <summary>
''' Prepares known information in a displayable format.
''' </summary>
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

View file

@ -0,0 +1,5 @@
Enum OperationType
BirthdayRole
BirthdayAnnounce
CommandDispatch
End Enum