mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-21 21:54:36 +00:00
Make diagnostic data user-accessible
This commit is contained in:
parent
3fc13efc57
commit
5012834073
11 changed files with 127 additions and 211 deletions
|
@ -38,6 +38,9 @@ Class BirthdayRoleUpdate
|
|||
End Try
|
||||
|
||||
' TODO metrics for role sets, unsets, announcements - and how to do that for singles too?
|
||||
|
||||
' Running GC now. Many long-lasting items have likely been discarded by now.
|
||||
GC.Collect()
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
|
@ -65,7 +68,6 @@ Class BirthdayRoleUpdate
|
|||
Dim channel As SocketTextChannel = Nothing
|
||||
Dim announce As (String, String)
|
||||
Dim announceping As Boolean
|
||||
Dim op As OperationStatus
|
||||
|
||||
' Skip processing of guild if local info has not yet been loaded
|
||||
If Not BotInstance.GuildCache.ContainsKey(guild.Id) Then Return
|
||||
|
@ -77,7 +79,6 @@ Class BirthdayRoleUpdate
|
|||
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)
|
||||
|
@ -91,24 +92,22 @@ Class BirthdayRoleUpdate
|
|||
' But first check if we are able to do so. Letting all requests fail instead will lead to rate limiting.
|
||||
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
|
||||
SyncLock gs
|
||||
gs.OperationLog = New OperationStatus((OperationStatus.OperationType.UpdateBirthdayRoleMembership, roleCheck.Item2))
|
||||
End SyncLock
|
||||
Return
|
||||
End If
|
||||
|
||||
Dim announcementList As IEnumerable(Of SocketGuildUser)
|
||||
Dim roleResult As (Integer, Integer) ' Role additions, removals
|
||||
' Do actual role updating
|
||||
Try
|
||||
announcementList = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
|
||||
SyncLock op
|
||||
op(OperationType.BirthdayRole) = New OperationInfo()
|
||||
End SyncLock
|
||||
Dim updateResult = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
|
||||
announcementList = updateResult.Item1
|
||||
roleResult = updateResult.Item2
|
||||
Catch ex As Discord.Net.HttpException
|
||||
SyncLock op
|
||||
op(OperationType.BirthdayRole) = New OperationInfo(ex)
|
||||
op(OperationType.BirthdayAnnounce) = Nothing
|
||||
SyncLock gs
|
||||
gs.OperationLog = New OperationStatus((OperationStatus.OperationType.UpdateBirthdayRoleMembership, ex.Message))
|
||||
End SyncLock
|
||||
If ex.HttpCode <> HttpStatusCode.Forbidden Then
|
||||
' Send unusual exceptions to calling method
|
||||
|
@ -117,9 +116,20 @@ Class BirthdayRoleUpdate
|
|||
Return
|
||||
End Try
|
||||
|
||||
Dim opResult1, opResult2 As (OperationStatus.OperationType, String)
|
||||
opResult1 = (OperationStatus.OperationType.UpdateBirthdayRoleMembership,
|
||||
$"Success: Added {roleResult.Item1} member(s), Removed {roleResult.Item2} member(s) from target role.")
|
||||
|
||||
If announcementList.Count <> 0 Then
|
||||
Await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList, op)
|
||||
Dim announceOpResult = Await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList)
|
||||
opResult2 = (OperationStatus.OperationType.SendBirthdayAnnouncementMessage, announceOpResult)
|
||||
Else
|
||||
opResult2 = (OperationStatus.OperationType.SendBirthdayAnnouncementMessage, "Announcement not considered.")
|
||||
End If
|
||||
|
||||
SyncLock gs
|
||||
gs.OperationLog = New OperationStatus(opResult1, opResult2)
|
||||
End SyncLock
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
|
@ -127,19 +137,19 @@ Class BirthdayRoleUpdate
|
|||
''' </summary>
|
||||
Private Function CheckCorrectRoleSettings(guild As SocketGuild, role As SocketRole) As (Boolean, String)
|
||||
If role Is Nothing Then
|
||||
Return (False, "Designated role not found or defined in guild")
|
||||
Return (False, "Failed: Designated role not found or defined.")
|
||||
End If
|
||||
|
||||
If Not guild.CurrentUser.GuildPermissions.ManageRoles Then
|
||||
Return (False, "Bot does not contain Manage Roles permission")
|
||||
Return (False, "Failed: Bot does not contain Manage Roles permission.")
|
||||
End If
|
||||
|
||||
' Check potential role order conflict
|
||||
If role.Position >= guild.CurrentUser.Hierarchy Then
|
||||
Return (False, "Targeted role is at or above bot's highest rank")
|
||||
Return (False, "Failed: Bot is beneath the designated role in the role hierarchy.")
|
||||
End If
|
||||
|
||||
Return (True, "Success")
|
||||
Return (True, Nothing)
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
|
@ -189,7 +199,7 @@ Class BirthdayRoleUpdate
|
|||
''' <returns>A list of users who had the birthday role applied. Use for the announcement message.</returns>
|
||||
Private Async Function UpdateGuildBirthdayRoles(g As SocketGuild,
|
||||
r As SocketRole,
|
||||
names As HashSet(Of ULong)) As Task(Of IEnumerable(Of SocketGuildUser))
|
||||
names As HashSet(Of ULong)) As Task(Of (IEnumerable(Of SocketGuildUser), (Integer, Integer)))
|
||||
' Check members currently with the role. Figure out which users to remove it from.
|
||||
Dim roleRemoves As New List(Of SocketGuildUser)
|
||||
Dim roleKeeps As New HashSet(Of ULong)
|
||||
|
@ -218,7 +228,7 @@ Class BirthdayRoleUpdate
|
|||
newBirthdays.Add(member)
|
||||
Next
|
||||
|
||||
Return newBirthdays
|
||||
Return (newBirthdays, (newBirthdays.Count, roleRemoves.Count))
|
||||
End Function
|
||||
|
||||
Public Const DefaultAnnounce = "Please wish a happy birthday to %n!"
|
||||
|
@ -231,13 +241,9 @@ Class BirthdayRoleUpdate
|
|||
Private Async Function AnnounceBirthdaysAsync(announce As (String, String),
|
||||
announcePing As Boolean,
|
||||
c As SocketTextChannel,
|
||||
names As IEnumerable(Of SocketGuildUser),
|
||||
op As OperationStatus) As Task
|
||||
names As IEnumerable(Of SocketGuildUser)) As Task(Of String)
|
||||
If c Is Nothing Then
|
||||
SyncLock op
|
||||
op(OperationType.BirthdayAnnounce) = New OperationInfo("Announcement channel missing or undefined")
|
||||
End SyncLock
|
||||
Return
|
||||
Return "Announcement channel is undefined."
|
||||
End If
|
||||
|
||||
Dim announceMsg As String
|
||||
|
@ -268,13 +274,9 @@ Class BirthdayRoleUpdate
|
|||
|
||||
Try
|
||||
Await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString()))
|
||||
SyncLock op
|
||||
op(OperationType.BirthdayAnnounce) = New OperationInfo()
|
||||
End SyncLock
|
||||
Return $"Successfully announced {names.Count} name(s)"
|
||||
Catch ex As Discord.Net.HttpException
|
||||
SyncLock op
|
||||
op(OperationType.BirthdayAnnounce) = New OperationInfo(ex)
|
||||
End SyncLock
|
||||
Return ex.Message
|
||||
End Try
|
||||
End Function
|
||||
End Class
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Imports BirthdayBot.CommandsCommon
|
||||
Imports Discord
|
||||
Imports Discord.Net
|
||||
Imports Discord.Webhook
|
||||
Imports Discord.WebSocket
|
||||
|
||||
Class BirthdayBot
|
||||
|
@ -10,7 +11,6 @@ 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
|
||||
|
@ -22,12 +22,13 @@ Class BirthdayBot
|
|||
Return Client
|
||||
End Get
|
||||
End Property
|
||||
|
||||
Friend ReadOnly Property GuildCache As ConcurrentDictionary(Of ULong, GuildStateInformation)
|
||||
Friend ReadOnly Property LogWebhook As DiscordWebhookClient
|
||||
|
||||
Public Sub New(conf As Configuration, dc As DiscordShardedClient)
|
||||
Config = conf
|
||||
Client = dc
|
||||
LogWebhook = New DiscordWebhookClient(conf.LogWebhook)
|
||||
GuildCache = New ConcurrentDictionary(Of ULong, GuildStateInformation)
|
||||
|
||||
_worker = New BackgroundServiceRunner(Me)
|
||||
|
@ -50,10 +51,6 @@ 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
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>BirthdayBot</RootNamespace>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.3.6</Version>
|
||||
<Version>1.4.0</Version>
|
||||
<Authors>Noi</Authors>
|
||||
<Company />
|
||||
<Description>Discord bot for birthday reminders.</Description>
|
||||
|
|
|
@ -8,7 +8,6 @@ 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
|
||||
|
||||
|
@ -36,8 +35,6 @@ Class Configuration
|
|||
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)()
|
||||
|
|
|
@ -19,7 +19,7 @@ Class GuildStateInformation
|
|||
Private _announceMsgPl As String
|
||||
Private _announcePing As Boolean
|
||||
Private ReadOnly _userCache As Dictionary(Of ULong, GuildUserSettings)
|
||||
Public ReadOnly Property OperationLog As OperationStatus
|
||||
Public Property OperationLog As OperationStatus
|
||||
|
||||
''' <summary>
|
||||
''' Gets a list of cached registered user information.
|
||||
|
|
48
BirthdayBot/Data/OperationStatus.vb
Normal file
48
BirthdayBot/Data/OperationStatus.vb
Normal file
|
@ -0,0 +1,48 @@
|
|||
Imports System.Text
|
||||
''' <summary>
|
||||
''' Holds information regarding the previous updating operation done on a guild including success/error information.
|
||||
''' </summary>
|
||||
Class OperationStatus
|
||||
Private ReadOnly _log As New Dictionary(Of OperationType, String)
|
||||
|
||||
Public ReadOnly Property Timestamp As DateTimeOffset
|
||||
|
||||
Sub New(ParamArray statuses() As (OperationType, String))
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
For Each status In statuses
|
||||
_log(status.Item1) = status.Item2
|
||||
Next
|
||||
End Sub
|
||||
|
||||
''' <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 As String = Nothing
|
||||
|
||||
If Not _log.TryGetValue(otype, info) Then
|
||||
report.AppendLine(prefix + "No data")
|
||||
Continue For
|
||||
End If
|
||||
|
||||
If info Is Nothing Then
|
||||
report.AppendLine(prefix + "Success")
|
||||
Else
|
||||
report.AppendLine(prefix + info)
|
||||
End If
|
||||
Next
|
||||
Return report.ToString()
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' Specifies the type of operation logged. These enum values are publicly displayed in the specified order.
|
||||
''' </summary>
|
||||
Public Enum OperationType
|
||||
UpdateBirthdayRoleMembership
|
||||
SendBirthdayAnnouncementMessage
|
||||
End Enum
|
||||
End Class
|
|
@ -1,75 +0,0 @@
|
|||
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
|
|
@ -1,37 +0,0 @@
|
|||
''' <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
|
|
@ -1,49 +0,0 @@
|
|||
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
|
|
@ -1,5 +0,0 @@
|
|||
Enum OperationType
|
||||
BirthdayRole
|
||||
BirthdayAnnounce
|
||||
CommandDispatch
|
||||
End Enum
|
|
@ -1,18 +1,22 @@
|
|||
Imports System.Text.RegularExpressions
|
||||
Imports Discord
|
||||
Imports Discord.WebSocket
|
||||
Imports NodaTime
|
||||
|
||||
Friend Class ManagerCommands
|
||||
Inherits CommandsCommon
|
||||
|
||||
Private Delegate Function ConfigSubcommand(param As String(), reqChannel As SocketTextChannel) As Task
|
||||
|
||||
Private _subcommands As Dictionary(Of String, ConfigSubcommand)
|
||||
Private _usercommands As Dictionary(Of String, CommandHandler)
|
||||
Private ReadOnly _subcommands As Dictionary(Of String, ConfigSubcommand)
|
||||
Private ReadOnly _usercommands As Dictionary(Of String, CommandHandler)
|
||||
|
||||
Public Overrides ReadOnly Property Commands As IEnumerable(Of (String, CommandHandler))
|
||||
Get
|
||||
Return New List(Of (String, CommandHandler)) From {
|
||||
("config", AddressOf CmdConfigDispatch),
|
||||
("override", AddressOf CmdOverride)
|
||||
("override", AddressOf CmdOverride),
|
||||
("status", AddressOf CmdStatus)
|
||||
}
|
||||
End Get
|
||||
End Property
|
||||
|
@ -338,6 +342,40 @@ Friend Class ManagerCommands
|
|||
Await action.Invoke(overparam, reqChannel, overuser)
|
||||
End Function
|
||||
|
||||
' Prints a status report useful for troubleshooting operational issues within a guild
|
||||
Private Async Function CmdStatus(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
|
||||
' Moderators only. As with config, silently drop if this check fails.
|
||||
If Not Instance.GuildCache(reqUser.Guild.Id).IsUserModerator(reqUser) Then Return
|
||||
|
||||
Dim result As New EmbedBuilder
|
||||
Dim optime As DateTimeOffset
|
||||
Dim optext As String
|
||||
Dim zone As String
|
||||
Dim gi = Instance.GuildCache(reqChannel.Guild.Id)
|
||||
SyncLock gi
|
||||
Dim opstat = gi.OperationLog
|
||||
optext = opstat.GetDiagStrings() ' !!! Bulk of output handled by this method
|
||||
optime = opstat.Timestamp
|
||||
zone = If(gi.TimeZone, "UTC")
|
||||
End SyncLock
|
||||
Dim shard = Instance.DiscordClient.GetShardIdFor(reqChannel.Guild)
|
||||
|
||||
' Calculate timestamp in current zone
|
||||
Dim ts As String = "Last update:"
|
||||
Dim zonedTimeInstant = SystemClock.Instance.GetCurrentInstant().InZone(DateTimeZoneProviders.Tzdb.GetZoneOrNull(zone))
|
||||
Dim timeAgoEstimate = DateTimeOffset.UtcNow - optime
|
||||
|
||||
With result
|
||||
.Title = "Background operation status"
|
||||
.Description = $"Shard: {shard}" + vbLf +
|
||||
$"Operation time: {Math.Round(timeAgoEstimate.TotalSeconds)} second(s) ago at {zonedTimeInstant}" + vbLf +
|
||||
"Report:" + vbLf +
|
||||
optext.TrimEnd()
|
||||
End With
|
||||
|
||||
Await reqChannel.SendMessageAsync(embed:=result.Build())
|
||||
End Function
|
||||
|
||||
#Region "Common/helper methods"
|
||||
Private Const RoleInputError = ":x: Unable to determine the given role."
|
||||
Private Shared ReadOnly RoleMention As New Regex("<@?&(?<snowflake>\d+)>", RegexOptions.Compiled)
|
||||
|
|
Loading…
Reference in a new issue