From 2c8a283c5bd971a68b28acc356304d1f3634cb1a Mon Sep 17 00:00:00 2001 From: Noikoio Date: Fri, 3 May 2019 20:34:04 -0700 Subject: [PATCH] Added bot moderator role option --- BirthdayBot/BirthdayBot.vb | 3 +- BirthdayBot/BirthdayBot.vbproj | 4 +- BirthdayBot/Data/GuildSettings.vb | 48 +++++++- BirthdayBot/UserInterface/HelpInfoCommands.vb | 29 ++--- BirthdayBot/UserInterface/ManagerCommands.vb | 111 ++++++++++++------ 5 files changed, 130 insertions(+), 65 deletions(-) diff --git a/BirthdayBot/BirthdayBot.vb b/BirthdayBot/BirthdayBot.vb index e921642..2f5b60b 100644 --- a/BirthdayBot/BirthdayBot.vb +++ b/BirthdayBot/BirthdayBot.vb @@ -110,12 +110,11 @@ Class BirthdayBot ' 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 isManager Then + If Not gi.IsUserModerator(author) Then If gi.IsUserBlockedAsync(author.Id).GetAwaiter().GetResult() Then Return End If diff --git a/BirthdayBot/BirthdayBot.vbproj b/BirthdayBot/BirthdayBot.vbproj index ae54e1f..e308f74 100644 --- a/BirthdayBot/BirthdayBot.vbproj +++ b/BirthdayBot/BirthdayBot.vbproj @@ -4,8 +4,8 @@ Exe BirthdayBot netcoreapp2.0 - 0.5.3 - 0.5.3.0 + 0.6.0 + 0.6.0.0 Noikoio Discord bot for birthday reminders. diff --git a/BirthdayBot/Data/GuildSettings.vb b/BirthdayBot/Data/GuildSettings.vb index c1ad872..842bed7 100644 --- a/BirthdayBot/Data/GuildSettings.vb +++ b/BirthdayBot/Data/GuildSettings.vb @@ -1,4 +1,5 @@ Imports System.Data.Common +Imports Discord.WebSocket Imports Npgsql Imports NpgsqlTypes @@ -12,6 +13,7 @@ Friend Class GuildSettings Private ReadOnly _db As Database Private _bdayRole As ULong? Private _announceCh As ULong? + Private _modRole As ULong? Private _tz As String Private _moderated As Boolean Private _userCache As Dictionary(Of ULong, GuildUserSettings) @@ -84,8 +86,7 @@ Friend Class GuildSettings End Property ''' - ''' Gets or sets if the server is in moderated mode. - ''' Updating this value updates the database. + ''' Gets whether the guild is in moderated mode. ''' Public ReadOnly Property IsModerated As Boolean Get @@ -93,6 +94,15 @@ Friend Class GuildSettings End Get End Property + ''' + ''' Gets the designated moderator role ID. + ''' + Public ReadOnly Property ModeratorRole As ULong? + Get + Return _modRole + End Get + End Property + ' Called by LoadSettingsAsync. Double-check ordinals when changes are made. Private Sub New(reader As DbDataReader, dbconfig As Database) _db = dbconfig @@ -107,6 +117,7 @@ Friend Class GuildSettings If Not reader.IsDBNull(2) Then _announceCh = CULng(reader.GetInt64(2)) _tz = If(reader.IsDBNull(3), Nothing, reader.GetString(3)) _moderated = reader.GetBoolean(4) + If Not reader.IsDBNull(5) Then _modRole = CULng(reader.GetInt64(5)) ' Get user information loaded up. Dim userresult = GuildUserSettings.GetGuildUsersAsync(dbconfig, GuildId) @@ -169,6 +180,19 @@ Friend Class GuildSettings End Using End Function + ''' + ''' Checks if the given user is a moderator either by having the Manage Server permission or + ''' being in the designated modeartor role. + ''' + Public Function IsUserModerator(user As SocketGuildUser) As Boolean + If user.GuildPermissions.ManageGuild Then Return True + If ModeratorRole.HasValue Then + If user.Roles.Where(Function(r) r.Id = ModeratorRole.Value).Count > 0 Then Return True + End If + + IsUserModerator = False + End Function + ''' ''' Adds the specified user to the block list, preventing them from issuing commands. ''' @@ -224,6 +248,11 @@ Friend Class GuildSettings Await UpdateDatabaseAsync() End Function + Public Async Function UpdateModeratorRoleAsync(roleId As ULong?) As Task + _modRole = roleId + Await UpdateDatabaseAsync() + End Function + #Region "Database" Public Const BackingTable = "settings" Public Const BackingTableBans = "banned_users" @@ -235,7 +264,8 @@ Friend Class GuildSettings "role_id bigint null, " + "channel_announce_id bigint null, " + "time_zone text null, " + - "moderated boolean not null default FALSE" + + "moderated boolean not null default FALSE, " + + "moderator_role bigint null" + ")" c.ExecuteNonQuery() End Using @@ -257,7 +287,7 @@ Friend Class GuildSettings Using db = Await dbsettings.OpenConnectionAsync() Using c = db.CreateCommand() ' Take note of ordinals for use in the constructor - c.CommandText = "select guild_id, role_id, channel_announce_id, time_zone, moderated " + + c.CommandText = "select guild_id, role_id, channel_announce_id, time_zone, moderated, moderator_role " + $"from {BackingTable} where guild_id = @Gid" c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = guild c.Prepare() @@ -291,7 +321,8 @@ Friend Class GuildSettings "role_id = @RoleId, " + "channel_announce_id = @ChannelId, " + "time_zone = @TimeZone, " + - "moderated = @Moderated " + + "moderated = @Moderated, " + + "moderator_role = @ModRole " + "where guild_id = @Gid" c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = GuildId With c.Parameters.Add("@RoleId", NpgsqlDbType.Bigint) @@ -316,6 +347,13 @@ Friend Class GuildSettings End If End With c.Parameters.Add("@Moderated", NpgsqlDbType.Boolean).Value = _moderated + With c.Parameters.Add("@ModRole", NpgsqlDbType.Bigint) + If ModeratorRole.HasValue Then + .Value = ModeratorRole.Value + Else + .Value = DBNull.Value + End If + End With c.Prepare() Await c.ExecuteNonQueryAsync() End Using diff --git a/BirthdayBot/UserInterface/HelpInfoCommands.vb b/BirthdayBot/UserInterface/HelpInfoCommands.vb index b435360..859ae53 100644 --- a/BirthdayBot/UserInterface/HelpInfoCommands.vb +++ b/BirthdayBot/UserInterface/HelpInfoCommands.vb @@ -43,13 +43,15 @@ Friend Class HelpInfoCommands ' Manager section Dim mpfx = cpfx + "config " - Dim managerField As New EmbedFieldBuilder With { - .Name = "Commands for server managers", + Dim moderatorField As New EmbedFieldBuilder With { + .Name = "Commands for server managers and bot moderators", .Value = $"{mpfx}role (role name or ID)`" + vbLf + " » Configures the role to apply to users having birthdays." + vbLf + $"{mpfx}channel (channel name or ID)`" + vbLf + " » Configures the channel to use for announcements. Leave blank to disable." + vbLf + + $"{mpfx}modrole (role name or ID)`" + vbLf + + " » Sets the designated role for bot moderators. Moderators can access `bb.config` and `bb.override`." + 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 + @@ -65,28 +67,21 @@ Friend Class HelpInfoCommands Dim helpNoManager As New EmbedBuilder helpNoManager.AddField(cmdField) - Dim helpManager As New EmbedBuilder - helpManager.AddField(cmdField) - helpManager.AddField(managerField) + Dim helpModerator As New EmbedBuilder + helpModerator.AddField(cmdField) + helpModerator.AddField(moderatorField) - Return (helpNoManager.Build(), helpManager.Build()) + Return (helpNoManager.Build(), helpModerator.Build()) End Function Private Async Function CmdHelp(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task - ' Determine if an additional message about an invalid role should be added. - Dim useFunctionMessage = False - Dim gs As GuildSettings + ' Determine if the user asking is a moderator + Dim showManagerCommands As Boolean SyncLock Instance.KnownGuilds - gs = Instance.KnownGuilds(reqChannel.Guild.Id) + showManagerCommands = Instance.KnownGuilds(reqChannel.Guild.Id).IsUserModerator(reqUser) End SyncLock - If Not gs.RoleId.HasValue Then - useFunctionMessage = True - End If - ' Determine if the user asking is a manager - Dim showManagerCommands = reqUser.GuildPermissions.ManageGuild - - Await reqChannel.SendMessageAsync("", embed:=If(showManagerCommands, _helpEmbedManager, _helpEmbed)) + Await reqChannel.SendMessageAsync(embed:=If(showManagerCommands, _helpEmbedManager, _helpEmbed)) End Function Private Async Function CmdHelpTzdata(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task diff --git a/BirthdayBot/UserInterface/ManagerCommands.vb b/BirthdayBot/UserInterface/ManagerCommands.vb index 2435aa4..ba9a13c 100644 --- a/BirthdayBot/UserInterface/ManagerCommands.vb +++ b/BirthdayBot/UserInterface/ManagerCommands.vb @@ -22,6 +22,7 @@ Friend Class ManagerCommands _subcommands = New Dictionary(Of String, ConfigSubcommand)(StringComparer.InvariantCultureIgnoreCase) From { {"role", AddressOf ScmdRole}, {"channel", AddressOf ScmdChannel}, + {"modrole", AddressOf ScmdModRole}, {"zone", AddressOf ScmdZone}, {"block", AddressOf ScmdBlock}, {"unblock", AddressOf ScmdBlock}, @@ -36,9 +37,14 @@ Friend Class ManagerCommands End Sub Private Async Function CmdConfigDispatch(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task - ' 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.") + ' Ignore those without the proper permissions. + ' Requires either the manage guild permission or to be in the moderators role + Dim allowed As Boolean + SyncLock Instance.KnownGuilds + allowed = Instance.KnownGuilds(reqUser.Guild.Id).IsUserModerator(reqUser) + End SyncLock + If Not allowed Then + Await reqChannel.SendMessageAsync(":x: This command may only be used by bot moderators.") Return End If @@ -47,6 +53,12 @@ Friend Class ManagerCommands Return End If + ' Special case: Restrict 'modrole' to only guild managers + If param(1).Equals("modrole", StringComparison.OrdinalIgnoreCase) And 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 + ' Subcommands get a subset of the parameters, to make things a little easier. Dim confparam(param.Length - 2) As String ' subtract one extra??? Array.Copy(param, 1, confparam, 0, param.Length - 1) @@ -59,47 +71,17 @@ Friend Class ManagerCommands End Function #Region "Configuration sub-commands" - Private Shared ReadOnly RoleMention As New Regex("<@?&(?\d+)>", RegexOptions.Compiled) - ' Birthday role set Private Async Function ScmdRole(param As String(), reqChannel As SocketTextChannel) As Task If param.Length <> 2 Then Await reqChannel.SendMessageAsync(":x: A role name, role mention, or ID value must be specified.") Return End If - Dim guild = reqChannel.Guild - Dim input = param(1) - Dim role As SocketRole = Nothing + Dim role = FindUserInputRole(param(1), guild) - ' Resembles a role mention? Strip it to the pure number - Dim rmatch = RoleMention.Match(input) - If rmatch.Success Then - input = rmatch.Groups("snowflake").Value - End If - - ' Attempt to get role by ID - Dim rid As ULong - If ULong.TryParse(input, rid) Then - role = guild.GetRole(rid) - Else - ' Reset the search value on the off chance there's a role name actually starting with "<&" and ending with ">" - input = param(1) - End If - - ' If not already found, attempt to search role by string name If role Is Nothing Then - For Each search In guild.Roles - If String.Equals(search.Name, input, StringComparison.InvariantCultureIgnoreCase) Then - role = search - Exit For - End If - Next - End If - - ' Final result - If role Is Nothing Then - Await reqChannel.SendMessageAsync(":x: Unable to determine the given role.") + Await reqChannel.SendMessageAsync(RoleInputError) Else SyncLock Instance.KnownGuilds Instance.KnownGuilds(guild.Id).UpdateRoleAsync(role.Id).Wait() @@ -152,6 +134,25 @@ Friend Class ManagerCommands End If End Function + ' Moderator role set + Private Async Function ScmdModRole(param As String(), reqChannel As SocketTextChannel) As Task + If param.Length <> 2 Then + Await reqChannel.SendMessageAsync(":x: A role name, role mention, or ID value must be specified.") + Return + End If + Dim guild = reqChannel.Guild + Dim role = FindUserInputRole(param(1), guild) + + If role Is Nothing Then + Await reqChannel.SendMessageAsync(RoleInputError) + Else + SyncLock Instance.KnownGuilds + Instance.KnownGuilds(guild.Id).UpdateModeratorRoleAsync(role.Id).Wait() + End SyncLock + Await reqChannel.SendMessageAsync($":white_check_mark: The moderator role is now **{role.Name}**.") + End If + End Function + ' Guild default time zone set/unset Private Async Function ScmdZone(param As String(), reqChannel As SocketTextChannel) As Task If param.Length = 1 Then @@ -263,10 +264,10 @@ Friend Class ManagerCommands ' Execute command as another user Private Async Function CmdOverride(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task - ' Managers only. Silently drop if the check fails. - If Not reqUser.GuildPermissions.ManageGuild Then - Return - End If + ' Moderators only. As with config, silently drop if this check fails. + SyncLock Instance.KnownGuilds + If Not Instance.KnownGuilds(reqUser.Guild.Id).IsUserModerator(reqUser) Then Return + End SyncLock If param.Length <> 3 Then Await reqChannel.SendMessageAsync(GenericError) @@ -302,4 +303,36 @@ Friend Class ManagerCommands Await reqChannel.SendMessageAsync($"Executing `{cmdsearch.ToLower()}` on behalf of {If(overuser.Nickname, overuser.Username)}:") Await action.Invoke(overparam, reqChannel, overuser) End Function + +#Region "Common/helper methods" + Private Const RoleInputError = ":x: Unable to determine the given role." + Private Shared ReadOnly RoleMention As New Regex("<@?&(?\d+)>", RegexOptions.Compiled) + + Private Function FindUserInputRole(inputStr As String, guild As SocketGuild) As SocketRole + ' Resembles a role mention? Strip it to the pure number + Dim input = inputStr + Dim rmatch = RoleMention.Match(input) + If rmatch.Success Then + input = rmatch.Groups("snowflake").Value + End If + + ' Attempt to get role by ID, or Nothing + Dim rid As ULong + If ULong.TryParse(input, rid) Then + Return guild.GetRole(rid) + Else + ' Reset the search value on the off chance there's a role name that actually resembles a role ping. + input = inputStr + End If + + ' If not already found, attempt to search role by string name + For Each search In guild.Roles + If String.Equals(search.Name, input, StringComparison.InvariantCultureIgnoreCase) Then + Return search + End If + Next + + Return Nothing + End Function +#End Region End Class \ No newline at end of file