diff --git a/BirthdayBot/BirthdayBot.vb b/BirthdayBot/BirthdayBot.vb index e76c903..6da6a2c 100644 --- a/BirthdayBot/BirthdayBot.vb +++ b/BirthdayBot/BirthdayBot.vb @@ -2,16 +2,17 @@ Option Explicit On Imports BirthdayBot.CommandsCommon Imports Discord +Imports Discord.Net Imports Discord.WebSocket Class BirthdayBot Const RoleWarningMsg As String = "Note: This bot does not have a role set or is unable to use the role specified. " + - "Update the designated role with `bb.config role (role name/ID). This bot cannot function without it." + "Update the designated role with `bb.config role (role name/ID)`. This bot cannot function without it." Private ReadOnly _dispatchCommands As Dictionary(Of String, CommandHandler) Private ReadOnly _cmdsUser As UserCommands - Private ReadOnly _cmdsHelp As HelpCommands + Private ReadOnly _cmdsHelp As HelpInfoCommands Private ReadOnly _cmdsMods As ManagerCommands Private WithEvents _client As DiscordSocketClient @@ -40,7 +41,7 @@ Class BirthdayBot For Each item In _cmdsUser.Commands _dispatchCommands.Add(item.Item1, item.Item2) Next - _cmdsHelp = New HelpCommands(Me, conf) + _cmdsHelp = New HelpInfoCommands(Me, conf) For Each item In _cmdsHelp.Commands _dispatchCommands.Add(item.Item1, item.Item2) Next @@ -102,15 +103,22 @@ Class BirthdayBot Dim channel = CType(msg.Channel, SocketTextChannel) Dim author = CType(msg.Author, SocketGuildUser) - Dim roleWarning As Boolean + ' Determine if it's something we're listening for. + ' Doing this first before the block check because a block check triggers a database query. + Dim command As CommandHandler = Nothing + If Not _dispatchCommands.TryGetValue(csplit(0).Substring(CommandPrefix.Length), command) Then + Return + End If ' Ban and role warning check + Dim roleWarning As Boolean + Dim isManager = author.GuildPermissions.ManageGuild SyncLock KnownGuilds Dim gi = KnownGuilds(channel.Guild.Id) ' Skip ban check if user is a manager - If Not author.GuildPermissions.ManageGuild Then - If gi.IsUserBannedAsync(author.Id).GetAwaiter().GetResult() Then + If Not isManager Then + If gi.IsUserBlockedAsync(author.Id).GetAwaiter().GetResult() Then Return End If End If @@ -118,18 +126,24 @@ Class BirthdayBot roleWarning = gi.RoleWarning End SyncLock - Dim h As CommandHandler = Nothing - If _dispatchCommands.TryGetValue(csplit(0).Substring(CommandPrefix.Length), h) Then - Try - Await h(csplit, channel, author) - If roleWarning Then + Try + If roleWarning Then + Try Await channel.SendMessageAsync(RoleWarningMsg) - End If - Catch ex As Exception + Catch ex As HttpException + ' Don't let this prevent the bot from continuing command execution. + End Try + End If + Await command(csplit, channel, author) + Catch ex As Exception + If TypeOf ex Is HttpException Then Return + Log("Error", ex.ToString()) + Try channel.SendMessageAsync(":x: An unknown error occurred. It has been reported to the bot owner.").Wait() - Log("Error", ex.ToString()) + Catch ex2 As HttpException + ' Fail silently. End Try - End If + End Try End If End If End Function diff --git a/BirthdayBot/BirthdayBot.vbproj b/BirthdayBot/BirthdayBot.vbproj index 1e95a0b..4cefe83 100644 --- a/BirthdayBot/BirthdayBot.vbproj +++ b/BirthdayBot/BirthdayBot.vbproj @@ -4,8 +4,8 @@ Exe BirthdayBot netcoreapp2.0 - 0.1.0 - 0.1.0.0 + 0.3.0 + 0.3.0.0 Noikoio Discord bot for birthday reminders. diff --git a/BirthdayBot/Data/GuildSettings.vb b/BirthdayBot/Data/GuildSettings.vb index 3c780e5..9585639 100644 --- a/BirthdayBot/Data/GuildSettings.vb +++ b/BirthdayBot/Data/GuildSettings.vb @@ -18,10 +18,31 @@ Friend Class GuildSettings Private _modded As Boolean Private _userCache As Dictionary(Of ULong, GuildUserSettings) + Private _roleWarning As Boolean + Private _roleLastWarning As New DateTimeOffset(DateTime.MinValue, TimeSpan.Zero) + Private Shared ReadOnly RoleWarningInterval As New TimeSpan(0, 10, 0) + ''' ''' Flag for notifying servers that the bot is unable to manipulate its role. + ''' Can be set at any time. Reading this will only return True once every 10 minutes, if at all. ''' Public Property RoleWarning As Boolean + Get + If _roleWarning = True Then + ' Only report a warning every so often. + If DateTimeOffset.UtcNow - _roleLastWarning > RoleWarningInterval Then + _roleLastWarning = DateTimeOffset.UtcNow + Return True + Else + Return False + End If + End If + Return False + End Get + Set(value As Boolean) + _roleWarning = value + End Set + End Property ''' ''' Gets a list of cached users. Use sparingly. @@ -67,14 +88,10 @@ Friend Class GuildSettings ''' Gets or sets if the server is in moderated mode. ''' Updating this value updates the database. ''' - Public Property IsModerated As Boolean + Public ReadOnly Property IsModerated As Boolean Get Return _modded End Get - Set(value As Boolean) - _modded = value - UpdateDatabaseAsync() - End Set End Property ' Called by LoadSettingsAsync. Double-check ordinals when changes are made. @@ -132,10 +149,11 @@ Friend Class GuildSettings End Function ''' - ''' Checks if the given user is banned from issuing commands. + ''' Checks if the given user is blocked from issuing commands. ''' If the server is in moderated mode, this always returns True. + ''' Does not check if the user is a manager. ''' - Public Async Function IsUserBannedAsync(userId As ULong) As Task(Of Boolean) + Public Async Function IsUserBlockedAsync(userId As ULong) As Task(Of Boolean) If IsModerated Then Return True Using db = Await _db.OpenConnectionAsync() @@ -154,10 +172,9 @@ Friend Class GuildSettings End Function ''' - ''' Bans the specified user from issuing commands. - ''' Does not check if the given user is already banned. + ''' Blocks the specified user from issuing commands. ''' - Public Async Function BanUserAsync(userId As ULong) As Task + Public Async Function BlockUserAsync(userId As ULong) As Task Using db = Await _db.OpenConnectionAsync() Using c = db.CreateCommand() c.CommandText = $"insert into {BackingTableBans} (guild_id, user_id) " + @@ -172,8 +189,7 @@ Friend Class GuildSettings End Function ''' - ''' Removes the specified user from the ban list. - ''' Does not check if the given user was not banned to begin with. + ''' Removes the specified user from the block list. ''' Public Async Function UnbanUserAsync(userId As ULong) As Task Using db = Await _db.OpenConnectionAsync() @@ -204,6 +220,11 @@ Friend Class GuildSettings Await UpdateDatabaseAsync() End Function + Public Async Function UpdateModeratedModeAsync(isModerated As Boolean) As Task + _modded = isModerated + Await UpdateDatabaseAsync() + End Function + #Region "Database" Public Const BackingTable = "settings" Public Const BackingTableBans = "banned_users" diff --git a/BirthdayBot/UserInterface/CommandsCommon.vb b/BirthdayBot/UserInterface/CommandsCommon.vb index 1d2082e..ae8282c 100644 --- a/BirthdayBot/UserInterface/CommandsCommon.vb +++ b/BirthdayBot/UserInterface/CommandsCommon.vb @@ -10,6 +10,7 @@ Imports NodaTime Friend MustInherit Class CommandsCommon Public Const CommandPrefix = "bb." Public Const GenericError = ":x: Invalid usage. Consult the help command." + Public Const BadUserError = ":x: Unable to find user. Specify their `@` mention or their ID." Public Const ExpectedNoParametersError = ":x: This command does not take parameters. Did you mean to use another?" Delegate Function CommandHandler(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task @@ -42,6 +43,11 @@ Friend MustInherit Class CommandsCommon Discord = inst.DiscordClient End Sub + ''' + ''' On command dispatcher initialization, it will retrieve all available commands through here. + ''' + Public MustOverride ReadOnly Property Commands As IEnumerable(Of (String, CommandHandler)) + ''' ''' Checks given time zone input. Returns a valid string for use with NodaTime. ''' @@ -59,7 +65,24 @@ Friend MustInherit Class CommandsCommon End Function ''' - ''' On command dispatcher initialization, it will retrieve all available commands through here. + ''' Given user input where a user-like parameter is expected, attempts to resolve to an ID value. + ''' Input must be a mention or explicit ID. No name resolution is done here. ''' - Public MustOverride ReadOnly Property Commands As IEnumerable(Of (String, CommandHandler)) + Protected Function TryGetUserId(input As String, ByRef result As ULong) As Boolean + Dim doParse As String + Dim m = UserMention.Match(input) + If m.Success Then + doParse = m.Groups(1).Value + Else + doParse = input + End If + + Dim resultVal As ULong + If ULong.TryParse(doParse, resultVal) Then + result = resultVal + Return True + End If + + Return False + End Function End Class diff --git a/BirthdayBot/UserInterface/HelpCommands.vb b/BirthdayBot/UserInterface/HelpInfoCommands.vb similarity index 51% rename from BirthdayBot/UserInterface/HelpCommands.vb rename to BirthdayBot/UserInterface/HelpInfoCommands.vb index 6adb731..9afd35b 100644 --- a/BirthdayBot/UserInterface/HelpCommands.vb +++ b/BirthdayBot/UserInterface/HelpInfoCommands.vb @@ -3,7 +3,7 @@ Option Explicit On Imports Discord Imports Discord.WebSocket -Friend Class HelpCommands +Friend Class HelpInfoCommands Inherits CommandsCommon Private ReadOnly _helpEmbed As Embed @@ -25,28 +25,20 @@ Friend Class HelpCommands End Property Private Function CreateHelpEmbed() As (EmbedBuilder, EmbedBuilder) - Dim title = "Help & About" - Dim description = "Birthday Bot: A utility to assist with acknowledging birthdays and other annual events." + vbLf + - "**Currently a work in progress. There will be bugs. Features may change or be removed.**" - Dim footer As New EmbedFooterBuilder With { - .Text = Discord.CurrentUser.Username, - .IconUrl = Discord.CurrentUser.GetAvatarUrl() - } - Dim cpfx = $"●`{CommandPrefix}" ' Normal section Dim cmdField As New EmbedFieldBuilder With { .Name = "Commands", .Value = - $"{cpfx}help`, `{CommandPrefix}info`, `{CommandPrefix}tzdata`" + vbLf + - $" » Various help messages." + vbLf + + $"{cpfx}help`, `{CommandPrefix}info`, `{CommandPrefix}help-tzdata`" + vbLf + + $" » Various help and informational messages." + vbLf + $"{cpfx}set (date) [zone]`" + vbLf + - $" » Registers your birth date, with optional time zone." + vbLf + - $" »» Examples: `{CommandPrefix}set jan-31 America/New_York`, `{CommandPrefix}set 15-aug Europe/Stockholm`." + vbLf + - $"{cpfx}set-tz (zone)`" + vbLf + - $" » Sets your local time zone. Only accepts certain values. See `{CommandPrefix}tzdata`." + vbLf + + $" » Registers your birth date. Time zone is optional." + vbLf + + $" »» Examples: `{CommandPrefix}set jan-31`, `{CommandPrefix}set 15-aug America/Los_Angeles`." + vbLf + + $"{cpfx}zone (zone)`" + vbLf + + $" » Sets your local time zone. See `{CommandPrefix}help-tzdata`." + vbLf + $"{cpfx}remove`" + vbLf + - $" » Removes all your information from this bot." + $" » Removes your information from this bot." } ' Manager section @@ -55,36 +47,27 @@ Friend Class HelpCommands .Name = "Commands for server managers", .Value = $"{mpfx}role (role name or ID)`" + vbLf + - " » Specifies which role to apply to users having birthdays." + vbLf + + " » Configures the role to apply to users having birthdays." + vbLf + $"{mpfx}channel (channel name or ID)`" + vbLf + - " » Sets the birthday and event announcement channel. Leave blank to disable announcements." + vbLf + - $"{mpfx}set-tz (time zone name)`" + vbLf + - " » Sets the default time zone to use with all dates. Leave blank to revert to default." + vbLf + - $" » Only accepts certain values. See `{CommandPrefix}tzdata`." + vbLf + - $"{mpfx}ban/unban (user mention or ID)`" + vbLf + - " » Restricts or reallows access to this bot for the given user." + vbLf + - $"{mpfx}ban-all/unban-all`" + vbLf + - " » Restricts or reallows access to this bot for all users. Server managers are exempt." + vbLf + - $"{cpfx}override (user ID) (regular command)`" + vbLf + - " » Performs a command on behalf of the given user." + " » Configures the channel to use for announcements. Leave blank to disable." + vbLf + + $"{mpfx}zone (time zone name)`" + vbLf + + " » Sets the default time zone for all dates that don't have their own zone set." + vbLf + + $" »» See `{CommandPrefix}help-tzdata`. Leave blank to set to UTC." + vbLf + + $"{mpfx}block/unblock (user mention or ID)`" + vbLf + + " » Prevents or allows usage of bot commands to the given user." + vbLf + + $"{mpfx}moderated on/off`" + vbLf + + " » Prevents or allows usage of bot commands to all users excluding managers." + vbLf + + $"{cpfx}override (user mention or ID) (command)`" + vbLf + + " » Performs a command on behalf of the given user." + vbLf + + " »» Command may be either `set`, `zone`, or `remove` plus appropriate parameters." } Dim helpNoManager As New EmbedBuilder - With helpNoManager - .Footer = footer - .Title = title - .Description = description - .AddField(cmdField) - End With + helpNoManager.AddField(cmdField) Dim helpManager As New EmbedBuilder - With helpManager - .Footer = footer - .Title = title - .Description = description - .AddField(cmdField) - .AddField(managerField) - End With + helpManager.AddField(cmdField) + helpManager.AddField(managerField) Return (helpNoManager, helpManager) End Function diff --git a/BirthdayBot/UserInterface/ManagerCommands.vb b/BirthdayBot/UserInterface/ManagerCommands.vb index 4d430c3..794bf43 100644 --- a/BirthdayBot/UserInterface/ManagerCommands.vb +++ b/BirthdayBot/UserInterface/ManagerCommands.vb @@ -22,17 +22,24 @@ Friend Class ManagerCommands _subcommands = New Dictionary(Of String, ConfigSubcommand) From { {"role", AddressOf ScmdRole}, {"channel", AddressOf ScmdChannel}, - {"set-tz", AddressOf ScmdSetTz}, - {"ban", AddressOf ScmdBanUnban}, - {"unban", AddressOf ScmdBanUnban}, - {"ban-all", AddressOf ScmdSetModerated}, - {"unban-all", AddressOf ScmdSetModerated} + {"zone", AddressOf ScmdZone}, + {"block", AddressOf ScmdBlock}, + {"unblock", AddressOf ScmdBlock}, + {"moderated", AddressOf ScmdModerated} } End Sub Private Async Function CmdConfigDispatch(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task - ' Managers only past this point. (This may have already been checked.) - If Not reqUser.GuildPermissions.ManageGuild Then Return + ' Managers only past this point. + If Not reqUser.GuildPermissions.ManageGuild Then + Await reqChannel.SendMessageAsync(":x: This command may only be used by those with the `Manage Server` permission.") + Return + End If + + If param.Length <> 3 Then + Await reqChannel.SendMessageAsync(GenericError) + Return + End If ' Subcommands get a subset of the parameters, to make things a little easier. Dim confparam(param.Length - 2) As String ' subtract 2??? @@ -137,7 +144,7 @@ Friend Class ManagerCommands End Function ' Guild default time zone set/unset - Private Async Function ScmdSetTz(param As String(), reqChannel As SocketTextChannel) As Task + Private Async Function ScmdZone(param As String(), reqChannel As SocketTextChannel) As Task If param.Length = 1 Then ' No extra parameter. Unset guild default time zone. SyncLock Instance.KnownGuilds @@ -175,49 +182,73 @@ Friend Class ManagerCommands End Function ' Block/unblock individual non-manager users from using commands. - Private Async Function ScmdBanUnban(param As String(), reqChannel As SocketTextChannel) As Task + Private Async Function ScmdBlock(param As String(), reqChannel As SocketTextChannel) As Task If param.Length <> 2 Then Await reqChannel.SendMessageAsync(GenericError) Return End If - Dim doBan As Boolean = param(0).ToLower() = "ban" ' True = ban, False = unban + Dim doBan As Boolean = param(0).ToLower() = "block" ' True = block, False = unblock - ' Parameter must be a mention or explicit ID. No name resolution. - Dim input = param(1) - Dim m = UserMention.Match(param(1)) - If m.Success Then input = m.Groups(1).Value Dim inputId As ULong - If Not ULong.TryParse(input, inputId) Then - Await reqChannel.SendMessageAsync(":x: Unable to find user. Specify their `@` mention or their ID.") + If Not TryGetUserId(param(1), inputId) Then + Await reqChannel.SendMessageAsync(BadUserError) Return End If SyncLock Instance.KnownGuilds Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id) - Dim isBanned = gi.IsUserBannedAsync(inputId).GetAwaiter().GetResult() + Dim isBanned = gi.IsUserBlockedAsync(inputId).GetAwaiter().GetResult() If doBan Then If Not isBanned Then - gi.BanUserAsync(inputId).Wait() - reqChannel.SendMessageAsync(":white_check_mark: User has been banned from using the bot").Wait() + gi.BlockUserAsync(inputId).Wait() + reqChannel.SendMessageAsync(":white_check_mark: User has been blocked.").Wait() Else - reqChannel.SendMessageAsync(":white_check_mark: The specified user is already banned.").Wait() + reqChannel.SendMessageAsync(":white_check_mark: User is already blocked.").Wait() End If Else If isBanned Then gi.UnbanUserAsync(inputId).Wait() - reqChannel.SendMessageAsync(":white_check_mark: User may now use the bot").Wait() + reqChannel.SendMessageAsync(":white_check_mark: User is now unblocked.").Wait() Else - reqChannel.SendMessageAsync(":white_check_mark: The specified user is not banned.").Wait() + reqChannel.SendMessageAsync(":white_check_mark: The specified user has not been blocked.").Wait() End If End If End SyncLock End Function - ' "ban/unban all" - Sets/unsets moderated mode. - Private Async Function ScmdSetModerated(param As String(), reqChannel As SocketTextChannel) As Task - Throw New NotImplementedException() + ' "moderated on/off" - Sets/unsets moderated mode. + Private Async Function ScmdModerated(param As String(), reqChannel As SocketTextChannel) As Task + If param.Length <> 2 Then + Await reqChannel.SendMessageAsync(GenericError) + Return + End If + + Dim parameter = param(1).ToLower() + Dim modSet As Boolean + If parameter = "on" Then + modSet = True + ElseIf parameter = "off" Then + modSet = False + Else + Await reqChannel.SendMessageAsync(GenericError) + Return + End If + + Dim currentSet As Boolean + + SyncLock Instance.KnownGuilds + Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id) + currentSet = gi.IsModerated + gi.UpdateModeratedModeAsync(modSet).Wait() + End SyncLock + + If currentSet = modSet Then + Await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode is already {parameter}.") + Else + Await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode has been turned {parameter}.") + End If End Function #End Region diff --git a/BirthdayBot/UserInterface/UserCommands.vb b/BirthdayBot/UserInterface/UserCommands.vb index ad6b731..c581197 100644 --- a/BirthdayBot/UserInterface/UserCommands.vb +++ b/BirthdayBot/UserInterface/UserCommands.vb @@ -11,7 +11,7 @@ Class UserCommands Get Return New List(Of (String, CommandHandler)) From { ("set", AddressOf CmdSet), - ("set-tz", AddressOf CmdSetTz), + ("zone", AddressOf CmdZone), ("remove", AddressOf CmdRemove) } End Get @@ -124,7 +124,7 @@ Class UserCommands End If End Function - Private Async Function CmdSetTz(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task + Private Async Function CmdZone(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task If param.Count <> 2 Then Await reqChannel.SendMessageAsync(GenericError) Return