mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 17:34:13 +00:00
Added operation diagnostic data
To assist with troubleshooting
This commit is contained in:
parent
b31813c210
commit
3fc13efc57
9 changed files with 253 additions and 46 deletions
|
@ -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 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
|
||||
|
||||
Dim announceNames As IEnumerable(Of SocketGuildUser) = Nothing
|
||||
If correctRoleSettings Then
|
||||
Dim announcementList As IEnumerable(Of SocketGuildUser)
|
||||
' Do actual role updating
|
||||
Try
|
||||
announceNames = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
|
||||
announcementList = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
|
||||
SyncLock op
|
||||
op(OperationType.BirthdayRole) = New OperationInfo()
|
||||
End SyncLock
|
||||
Catch ex As Discord.Net.HttpException
|
||||
If ex.HttpCode = HttpStatusCode.Forbidden Then
|
||||
gotForbidden = True
|
||||
Else
|
||||
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
|
||||
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
|
||||
|
||||
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>
|
||||
|
@ -221,8 +231,14 @@ Class BirthdayRoleUpdate
|
|||
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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
75
BirthdayBot/Diagnostics/DiagnosticCommands.vb
Normal file
75
BirthdayBot/Diagnostics/DiagnosticCommands.vb
Normal 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
|
37
BirthdayBot/Diagnostics/OperationInfo.vb
Normal file
37
BirthdayBot/Diagnostics/OperationInfo.vb
Normal 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
|
49
BirthdayBot/Diagnostics/OperationStatus.vb
Normal file
49
BirthdayBot/Diagnostics/OperationStatus.vb
Normal 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
|
5
BirthdayBot/Diagnostics/OperationType.vb
Normal file
5
BirthdayBot/Diagnostics/OperationType.vb
Normal file
|
@ -0,0 +1,5 @@
|
|||
Enum OperationType
|
||||
BirthdayRole
|
||||
BirthdayAnnounce
|
||||
CommandDispatch
|
||||
End Enum
|
Loading…
Reference in a new issue