From 25d691c4e91b751330b0cd26b264d122d73c6de7 Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Thu, 29 Mar 2018 15:35:00 +0200 Subject: [PATCH] Work on permission system, along with initial work on database(postgres) system --- DeukBot4/Database/DatabaseConnection.cs | 28 +++++ DeukBot4/Database/DatabaseInitializer.cs | 25 +++++ DeukBot4/Database/DatabaseRolePermissions.cs | 51 +++++++++ DeukBot4/DeukBot4.csproj | 1 + .../CommandHandler/CommandHandler.cs | 2 +- .../RequestStructure/CommandRequest.cs | 14 ++- .../CommandHandler/RolePermissionCommands.cs | 100 ++++++++++++++++++ DeukBot4/MessageHandlers/MainHandler.cs | 12 ++- .../Permissions/PermissionLevels.cs | 3 +- .../Permissions/PermissionValidator.cs | 95 +++++++++++++++-- DeukBot4/Program.cs | 4 + DeukBot4/Settings.cs | 2 + DeukBot4/Utilities/LongExtensions.cs | 15 +++ 13 files changed, 337 insertions(+), 15 deletions(-) create mode 100644 DeukBot4/Database/DatabaseConnection.cs create mode 100644 DeukBot4/Database/DatabaseInitializer.cs create mode 100644 DeukBot4/Database/DatabaseRolePermissions.cs create mode 100644 DeukBot4/MessageHandlers/CommandHandler/RolePermissionCommands.cs create mode 100644 DeukBot4/Utilities/LongExtensions.cs diff --git a/DeukBot4/Database/DatabaseConnection.cs b/DeukBot4/Database/DatabaseConnection.cs new file mode 100644 index 0000000..f9372fa --- /dev/null +++ b/DeukBot4/Database/DatabaseConnection.cs @@ -0,0 +1,28 @@ +using System; +using DeukBot4.MessageHandlers.Permissions; +using Npgsql; + +namespace DeukBot4.Database +{ + public class DatabaseConnection : IDisposable + { + public static string ConnectionString { private get; set; } + private readonly NpgsqlConnection _connection; + + public DatabaseConnection() + { + _connection = new NpgsqlConnection(ConnectionString); + _connection.Open(); + } + + public void Dispose() + { + _connection.Dispose(); + } + + public static implicit operator NpgsqlConnection(DatabaseConnection conn) + { + return conn._connection; + } + } +} \ No newline at end of file diff --git a/DeukBot4/Database/DatabaseInitializer.cs b/DeukBot4/Database/DatabaseInitializer.cs new file mode 100644 index 0000000..e50bafe --- /dev/null +++ b/DeukBot4/Database/DatabaseInitializer.cs @@ -0,0 +1,25 @@ +using Npgsql; + +namespace DeukBot4.Database +{ + public static class DatabaseInitializer + { + public static void Initialize() + { + using (var conn = new DatabaseConnection()) + { + using (var cmd = new NpgsqlCommand()) + { + cmd.Connection = conn; + cmd.CommandText = "CREATE TABLE IF NOT EXISTS permission_roles (" + + "server_id bigint NOT NULL," + + "role_id bigint NOT NULL," + + "permission_level smallint NOT NULL," + + "PRIMARY KEY(server_id, role_id)" + + ")"; + cmd.ExecuteNonQuery(); + } + } + } + } +} \ No newline at end of file diff --git a/DeukBot4/Database/DatabaseRolePermissions.cs b/DeukBot4/Database/DatabaseRolePermissions.cs new file mode 100644 index 0000000..1b3a8e1 --- /dev/null +++ b/DeukBot4/Database/DatabaseRolePermissions.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using DeukBot4.MessageHandlers.Permissions; +using DeukBot4.Utilities; +using Npgsql; + +namespace DeukBot4.Database +{ + public static class DatabaseRolePermissions + { + public static async Task SetRolePermission(ulong serverId, ulong roleId, PermissionLevel permissionLevel) + { + using (var conn = new DatabaseConnection()) + { + using (var cmd = new NpgsqlCommand()) + { + cmd.Connection = conn; + cmd.CommandText = "INSERT INTO permission_roles (server_id, role_id, permission_level)" + + "VALUES (@server_id, @role_id, @permission_level) " + + "ON CONFLICT (server_id, role_id) DO UPDATE SET permission_level = @permission_level"; + cmd.Parameters.AddWithValue("server_id", serverId.ToLong()); + cmd.Parameters.AddWithValue("role_id", roleId.ToLong()); + cmd.Parameters.AddWithValue("permission_level", (sbyte)permissionLevel); + await cmd.ExecuteNonQueryAsync(); + } + } + } + + public static async Task GetRolePermission(ulong serverId, ulong roleId) + { + using (var conn = new DatabaseConnection()) + { + using (var cmd = new NpgsqlCommand()) + { + cmd.Connection = conn; + cmd.CommandText = + "SELECT permission_level FROM permission_roles WHERE server_id = @server_id AND role_id = @role_id"; + cmd.Parameters.AddWithValue("server_id", serverId.ToLong()); + cmd.Parameters.AddWithValue("role_id", roleId.ToLong()); + + var reader = cmd.ExecuteReader(); + while (await reader.ReadAsync()) + { + return (PermissionLevel)(sbyte)reader.GetInt16(0); + } + + return (PermissionLevel)sbyte.MinValue; + } + } + } + } +} \ No newline at end of file diff --git a/DeukBot4/DeukBot4.csproj b/DeukBot4/DeukBot4.csproj index 08f5001..4a841ef 100644 --- a/DeukBot4/DeukBot4.csproj +++ b/DeukBot4/DeukBot4.csproj @@ -7,5 +7,6 @@ + \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs b/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs index b29163d..e78a461 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs @@ -39,7 +39,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler public static async Task HandleMessage(SocketMessage message) { if (message.Content[0] != CommandTrigger) return; - var req = CommandRequest.Create(message); + var req = await CommandRequest.Create(message); var resultCode = req.Item2; if (resultCode == CommandRequest.RequestCode.Invalid) { diff --git a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs index 5090494..a7e6e43 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using System.Threading.Tasks; using DeukBot4.MessageHandlers.Permissions; using Discord.WebSocket; @@ -30,7 +31,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure OK, Invalid, Forbidden } - public static (CommandRequest, RequestCode) Create(SocketMessage message) + public static async Task<(CommandRequest, RequestCode)> Create(SocketMessage message) { var originalMessage = message; var content = message.Content; @@ -45,7 +46,16 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure return (null, RequestCode.Invalid); } - var permission = PermissionValidator.GetUserPermissionLevel(message); + PermissionLevel permission; + try + { + permission = await PermissionValidator.GetUserPermissionLevel(message); + } + catch (Exception e) + { + await Logger.LogError(e.Message); + return (null, RequestCode.Forbidden); + } if (permission < command.Permission) { return (null, RequestCode.Forbidden); diff --git a/DeukBot4/MessageHandlers/CommandHandler/RolePermissionCommands.cs b/DeukBot4/MessageHandlers/CommandHandler/RolePermissionCommands.cs new file mode 100644 index 0000000..d27d8f0 --- /dev/null +++ b/DeukBot4/MessageHandlers/CommandHandler/RolePermissionCommands.cs @@ -0,0 +1,100 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using DeukBot4.Database; +using DeukBot4.MessageHandlers.CommandHandler.RequestStructure; +using DeukBot4.MessageHandlers.Permissions; +using Discord; + +namespace DeukBot4.MessageHandlers.CommandHandler +{ + public class RolePermissionCommands : CommandContainerBase + { + public override string Name => "Role Related"; + + [Command("roles", PermissionLevel.Admin)] + [CommandHelp("Lists all roles on the server along with their IDs", "Lists all roles on the server along with their IDs")] + public async Task ListServerRoles(CommandRequest request) + { + var channel = request.OriginalMessage.Channel; + if (!(channel is IGuildChannel serverChannel)) + return; + + var roles = serverChannel.Guild.Roles; + var sb = new StringBuilder(); + foreach (var role in roles) + { + sb.Append($"``{role.Name}`` --> {role.Id}\n"); + } + await channel.SendMessageAsync(sb.ToString()); + } + + [Command("adminrole", PermissionLevel.ServerOwner)] + [CommandParameters(new []{ParameterMatcher.ParameterType.Number})] + public async Task SetAdminRole(CommandRequest request) + { + await SetRolePermission(request, PermissionLevel.Admin); + } + + [Command("moderatorrole", PermissionLevel.ServerOwner)] + [CommandParameters(new []{ParameterMatcher.ParameterType.Number})] + public async Task SetModRole(CommandRequest request) + { + await SetRolePermission(request, PermissionLevel.Moderator); + } + + [Command("helperrole", PermissionLevel.ServerOwner)] + [CommandParameters(new []{ParameterMatcher.ParameterType.Number})] + public async Task SetHelperRole(CommandRequest request) + { + await SetRolePermission(request, PermissionLevel.Helper); + } + + [Command("bannedrole", PermissionLevel.ServerOwner)] + [CommandParameters(new []{ParameterMatcher.ParameterType.Number})] + public async Task SetBannedRole(CommandRequest request) + { + await SetRolePermission(request, PermissionLevel.Banned); + } + + + private static async Task SetRolePermission(CommandRequest request, PermissionLevel permissionLevel) + { + var channel = request.OriginalMessage.Channel; + if (!(channel is IGuildChannel serverChannel)) + return; + + if (request.Parameters.Length == 0) + { + await request.OriginalMessage.Channel.SendMessageAsync( + $"You did not give a valid role ID. Use ``!roles`` to list all current server roles, along with their ids"); + return; + } + + if (!ulong.TryParse(request.Parameters[0].AsString(), out var roleId)) + { + await request.OriginalMessage.Channel.SendMessageAsync( + $"You did not give a valid role ID. Use ``!roles`` to list all current server roles, along with their ids"); + return; + } + + var role = serverChannel.Guild.GetRole(roleId); + if (role == null) + { + await request.OriginalMessage.Channel.SendMessageAsync("No role with that id exists on this server"); + return; + } + + try + { + await DatabaseRolePermissions.SetRolePermission(serverChannel.GuildId, roleId, permissionLevel); + PermissionValidator.UpdateCache(serverChannel.GuildId, roleId, permissionLevel); + } + catch(Exception e) + { + await Logger.LogError(e.Message); + } + } + + } +} \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/MainHandler.cs b/DeukBot4/MessageHandlers/MainHandler.cs index cb87857..ed8a588 100644 --- a/DeukBot4/MessageHandlers/MainHandler.cs +++ b/DeukBot4/MessageHandlers/MainHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Discord.WebSocket; namespace DeukBot4.MessageHandlers @@ -7,7 +8,14 @@ namespace DeukBot4.MessageHandlers { public static async Task HandleMessage(SocketMessage message) { - await CommandHandler.CommandHandler.HandleMessage(message); + try + { + await CommandHandler.CommandHandler.HandleMessage(message); + } + catch (Exception e) + { + await Logger.LogError(e.ToString()); + } } } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/Permissions/PermissionLevels.cs b/DeukBot4/MessageHandlers/Permissions/PermissionLevels.cs index 1ce06e4..84a9f19 100644 --- a/DeukBot4/MessageHandlers/Permissions/PermissionLevels.cs +++ b/DeukBot4/MessageHandlers/Permissions/PermissionLevels.cs @@ -8,6 +8,7 @@ Helper = 20, Moderator = 40, Admin = 60, - Owner = 100 + ServerOwner = 80, + BotCreator = 100 } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs b/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs index aa6411d..ed272c0 100644 --- a/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs +++ b/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs @@ -1,27 +1,104 @@ -using DeukBot4.MessageHandlers.CommandHandler.RequestStructure; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DeukBot4.Database; +using DeukBot4.MessageHandlers.CommandHandler.RequestStructure; +using Discord; using Discord.WebSocket; namespace DeukBot4.MessageHandlers.Permissions { public static class PermissionValidator { - public static PermissionLevel GetUserPermissionLevel(SocketMessage message) + private static readonly ConcurrentDictionary> PermissionCache = + new ConcurrentDictionary>(); + + private static async Task GetDatabasePermissionLevelForRole(ulong serverId, ulong roleId) + { + if (PermissionCache.TryGetValue(serverId, out var serverPermissions)) + { + if (serverPermissions.TryGetValue(roleId, out var permissions)) + { + return permissions; + } + } + else + { + PermissionCache.TryAdd(serverId, new ConcurrentDictionary()); + } + + serverPermissions = PermissionCache[serverId]; + try + { + var permission = await DatabaseRolePermissions.GetRolePermission(serverId, roleId); + serverPermissions.TryAdd(serverId, permission); + return permission; + } + catch(Exception e) + { + await Logger.LogError(e.ToString()); + return PermissionLevel.Everyone; + } + } + + public static void UpdateCache(ulong serverId, ulong roleId, PermissionLevel level) + { + if (PermissionCache.TryGetValue(serverId, out var serverPermissions)) + { + if (serverPermissions.ContainsKey(roleId)) + { + serverPermissions[roleId] = level; + } + else + { + serverPermissions.TryAdd(roleId, level); + } + } + } + + private static async Task GetDatabasePermissionLevel(ulong serverId, ulong[] roles) + { + var highestRole = (PermissionLevel)sbyte.MinValue; + foreach (var role in roles) + { + var perms = await GetDatabasePermissionLevelForRole(serverId, role); + if (perms > highestRole) + { + highestRole = perms; + } + } + + if (highestRole == (PermissionLevel) sbyte.MinValue) + { + return PermissionLevel.Everyone; + } + + return highestRole; + } + + public static async Task GetUserPermissionLevel(SocketMessage message) { if (message.Author.Id == Program.Settings.OwnerId) { - return PermissionLevel.Owner; + return PermissionLevel.BotCreator; } if (message.Author.IsBot) { return PermissionLevel.Bot; } - return PermissionLevel.Everyone; - } + if (!(message.Channel is IGuildChannel serverChannel)) + return PermissionLevel.Everyone; - public static bool CanUse(this CommandRequest req) - { - var level = GetUserPermissionLevel(req.OriginalMessage); - return level >= req.Command.Permission; + if (serverChannel.Guild.OwnerId == message.Author.Id) + return PermissionLevel.ServerOwner; + + if (!(message.Author is IGuildUser user)) + return PermissionLevel.Everyone; + + var perms = await GetDatabasePermissionLevel(serverChannel.GuildId, user.RoleIds.ToArray()); + return perms; } } } \ No newline at end of file diff --git a/DeukBot4/Program.cs b/DeukBot4/Program.cs index 041a870..a97739f 100644 --- a/DeukBot4/Program.cs +++ b/DeukBot4/Program.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using DeukBot4.Database; using DeukBot4.MessageHandlers; using DeukBot4.MessageHandlers.CommandHandler; using Discord; @@ -21,6 +22,9 @@ namespace DeukBot4 private static async Task MainAsync() { Settings = Settings.FromJsonFile("settings.json"); + DatabaseConnection.ConnectionString = Settings.DatabaseConnectionString; + + DatabaseInitializer.Initialize(); CommandHandler.Build(); Client = new DiscordSocketClient(); diff --git a/DeukBot4/Settings.cs b/DeukBot4/Settings.cs index 669c94d..65e9b77 100644 --- a/DeukBot4/Settings.cs +++ b/DeukBot4/Settings.cs @@ -11,6 +11,8 @@ namespace DeukBot4 public string Username { get; private set; } [JsonProperty] public ulong OwnerId { get; private set; } + [JsonProperty] + public string DatabaseConnectionString { get; private set; } public static Settings FromJsonFile(string filepath) diff --git a/DeukBot4/Utilities/LongExtensions.cs b/DeukBot4/Utilities/LongExtensions.cs new file mode 100644 index 0000000..21faa92 --- /dev/null +++ b/DeukBot4/Utilities/LongExtensions.cs @@ -0,0 +1,15 @@ +namespace DeukBot4.Utilities +{ + public static class LongExtensions + { + public static ulong ToUlong(this long l) + { + return unchecked((ulong)(l - long.MinValue)); + } + + public static long ToLong(this ulong l) + { + return unchecked((long)l + long.MinValue); + } + } +} \ No newline at end of file