mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 17:34:13 +00:00
Merge branch 'noguildcache'
This commit is contained in:
commit
c77163cab3
19 changed files with 678 additions and 886 deletions
|
@ -12,14 +12,15 @@ namespace BirthdayBot
|
||||||
class BackgroundServiceRunner
|
class BackgroundServiceRunner
|
||||||
{
|
{
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
// Amount of idle time between each round of task execution, in seconds.
|
|
||||||
const int Interval = 8 * 60;
|
|
||||||
|
|
||||||
// Amount of time between start and first round of processing, in seconds.
|
// Amount of time between start and first round of processing, in seconds.
|
||||||
const int StartDelay = 60;
|
const int StartDelay = 3 * 60; // 3 minutes
|
||||||
|
|
||||||
|
// Amount of idle time between each round of task execution, in seconds.
|
||||||
|
const int Interval = 5 * 60; // 5 minutes
|
||||||
#else
|
#else
|
||||||
const int Interval = 10;
|
// Short intervals for testing
|
||||||
const int StartDelay = 15;
|
const int StartDelay = 20;
|
||||||
|
const int Interval = 20;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const string LogName = nameof(BackgroundServiceRunner);
|
const string LogName = nameof(BackgroundServiceRunner);
|
||||||
|
|
|
@ -23,9 +23,11 @@ namespace BirthdayBot.BackgroundServices
|
||||||
public override async Task OnTick()
|
public override async Task OnTick()
|
||||||
{
|
{
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
foreach (var guild in BotInstance.DiscordClient.Guilds)
|
|
||||||
|
// Work on each shard concurrently; guilds within each shard synchronously
|
||||||
|
foreach (var shard in BotInstance.DiscordClient.Shards)
|
||||||
{
|
{
|
||||||
tasks.Add(ProcessGuildAsync(guild));
|
tasks.Add(ProcessShardAsync(shard));
|
||||||
}
|
}
|
||||||
var alltasks = Task.WhenAll(tasks);
|
var alltasks = Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
@ -40,6 +42,7 @@ namespace BirthdayBot.BackgroundServices
|
||||||
{
|
{
|
||||||
Log($"{exs.InnerExceptions.Count} exception(s) during bulk processing!");
|
Log($"{exs.InnerExceptions.Count} exception(s) during bulk processing!");
|
||||||
// TODO needs major improvements. output to file?
|
// TODO needs major improvements. output to file?
|
||||||
|
foreach (var iex in exs.InnerExceptions) Log(iex.Message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -59,6 +62,39 @@ namespace BirthdayBot.BackgroundServices
|
||||||
/// <returns>Diagnostic data in string form.</returns>
|
/// <returns>Diagnostic data in string form.</returns>
|
||||||
public async Task<string> SingleProcessGuildAsync(SocketGuild guild) => (await ProcessGuildAsync(guild)).Export();
|
public async Task<string> SingleProcessGuildAsync(SocketGuild guild) => (await ProcessGuildAsync(guild)).Export();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by <see cref="OnTick"/>, processes all guilds within a shard synchronously.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ProcessShardAsync(DiscordSocketClient shard)
|
||||||
|
{
|
||||||
|
if (shard.ConnectionState != Discord.ConnectionState.Connected)
|
||||||
|
{
|
||||||
|
Log($"Shard {shard.ShardId} (with {shard.Guilds.Count} guilds) processing stopped - shard disconnected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var exs = new List<Exception>();
|
||||||
|
foreach (var guild in shard.Guilds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check if shard remains available
|
||||||
|
if (shard.ConnectionState != Discord.ConnectionState.Connected)
|
||||||
|
{
|
||||||
|
Log($"Shard {shard.ShardId} (with {shard.Guilds.Count} guilds) processing interrupted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ProcessGuildAsync(guild);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Catch all exceptions per-guild but continue processing, throw at end
|
||||||
|
exs.Add(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log($"Shard {shard.ShardId} (with {shard.Guilds.Count} guilds) processing completed.");
|
||||||
|
if (exs.Count != 0) throw new AggregateException(exs);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Main method where actual guild processing occurs.
|
/// Main method where actual guild processing occurs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -66,23 +102,20 @@ namespace BirthdayBot.BackgroundServices
|
||||||
{
|
{
|
||||||
var diag = new PGDiagnostic();
|
var diag = new PGDiagnostic();
|
||||||
|
|
||||||
// Skip processing of guild if local info has not yet been loaded
|
// Load guild information - stop if there is none (bot never previously used in guild)
|
||||||
if (!BotInstance.GuildCache.TryGetValue(guild.Id, out var gs))
|
var gc = await GuildConfiguration.LoadAsync(guild.Id, true);
|
||||||
{
|
if (gc == null) return diag;
|
||||||
diag.FetchCachedGuild = "Server information not yet loaded by the bot. Try again later.";
|
|
||||||
return diag;
|
|
||||||
}
|
|
||||||
diag.FetchCachedGuild = null;
|
|
||||||
|
|
||||||
// Check if role settings are correct before continuing with further processing
|
// Check if role settings are correct before continuing with further processing
|
||||||
SocketRole role = null;
|
SocketRole role = null;
|
||||||
if (gs.RoleId.HasValue) role = guild.GetRole(gs.RoleId.Value);
|
if (gc.RoleId.HasValue) role = guild.GetRole(gc.RoleId.Value);
|
||||||
diag.RoleCheck = CheckCorrectRoleSettings(guild, role);
|
diag.RoleCheck = CheckCorrectRoleSettings(guild, role);
|
||||||
if (diag.RoleCheck != null) return diag;
|
if (diag.RoleCheck != null) return diag;
|
||||||
|
|
||||||
// Determine who's currently having a birthday
|
// Determine who's currently having a birthday
|
||||||
var users = gs.Users;
|
//await guild.DownloadUsersAsync();
|
||||||
var tz = gs.TimeZone;
|
var users = await GuildUserConfiguration.LoadAllAsync(guild.Id);
|
||||||
|
var tz = gc.TimeZone;
|
||||||
var birthdays = GetGuildCurrentBirthdays(users, tz);
|
var birthdays = GetGuildCurrentBirthdays(users, tz);
|
||||||
// Note: Don't quit here if zero people are having birthdays. Roles may still need to be removed by BirthdayApply.
|
// Note: Don't quit here if zero people are having birthdays. Roles may still need to be removed by BirthdayApply.
|
||||||
diag.CurrentBirthdays = birthdays.Count.ToString();
|
diag.CurrentBirthdays = birthdays.Count.ToString();
|
||||||
|
@ -103,10 +136,10 @@ namespace BirthdayBot.BackgroundServices
|
||||||
diag.RoleApply = null;
|
diag.RoleApply = null;
|
||||||
|
|
||||||
// Birthday announcement
|
// Birthday announcement
|
||||||
var announce = gs.AnnounceMessages;
|
var announce = gc.AnnounceMessages;
|
||||||
var announceping = gs.AnnouncePing;
|
var announceping = gc.AnnouncePing;
|
||||||
SocketTextChannel channel = null;
|
SocketTextChannel channel = null;
|
||||||
if (gs.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gs.AnnounceChannelId.Value);
|
if (gc.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gc.AnnounceChannelId.Value);
|
||||||
if (announcementList.Count() != 0)
|
if (announcementList.Count() != 0)
|
||||||
{
|
{
|
||||||
var announceResult = await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList);
|
var announceResult = await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList);
|
||||||
|
@ -145,7 +178,7 @@ namespace BirthdayBot.BackgroundServices
|
||||||
/// Gets all known users from the given guild and returns a list including only those who are
|
/// Gets all known users from the given guild and returns a list including only those who are
|
||||||
/// currently experiencing a birthday in the respective time zone.
|
/// currently experiencing a birthday in the respective time zone.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserSettings> guildUsers, string defaultTzStr)
|
private HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string defaultTzStr)
|
||||||
{
|
{
|
||||||
var birthdayUsers = new HashSet<ulong>();
|
var birthdayUsers = new HashSet<ulong>();
|
||||||
|
|
||||||
|
@ -271,7 +304,6 @@ namespace BirthdayBot.BackgroundServices
|
||||||
{
|
{
|
||||||
const string DefaultValue = "--";
|
const string DefaultValue = "--";
|
||||||
|
|
||||||
public string FetchCachedGuild = DefaultValue;
|
|
||||||
public string RoleCheck = DefaultValue;
|
public string RoleCheck = DefaultValue;
|
||||||
public string CurrentBirthdays = DefaultValue;
|
public string CurrentBirthdays = DefaultValue;
|
||||||
public string RoleApply = DefaultValue;
|
public string RoleApply = DefaultValue;
|
||||||
|
@ -282,7 +314,6 @@ namespace BirthdayBot.BackgroundServices
|
||||||
{
|
{
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
result.AppendLine("Test result:");
|
result.AppendLine("Test result:");
|
||||||
result.AppendLine("Fetch guild information: " + (FetchCachedGuild ?? ":white_check_mark:"));
|
|
||||||
result.AppendLine("Check role permissions: " + (RoleCheck ?? ":white_check_mark:"));
|
result.AppendLine("Check role permissions: " + (RoleCheck ?? ":white_check_mark:"));
|
||||||
result.AppendLine("Number of known users currently with a birthday: " + CurrentBirthdays);
|
result.AppendLine("Number of known users currently with a birthday: " + CurrentBirthdays);
|
||||||
result.AppendLine("Role application process: " + (RoleApply ?? ":white_check_mark:"));
|
result.AppendLine("Role application process: " + (RoleApply ?? ":white_check_mark:"));
|
||||||
|
|
|
@ -14,8 +14,7 @@ namespace BirthdayBot.BackgroundServices
|
||||||
public async override Task OnTick()
|
public async override Task OnTick()
|
||||||
{
|
{
|
||||||
var count = BotInstance.DiscordClient.Guilds.Count;
|
var count = BotInstance.DiscordClient.Guilds.Count;
|
||||||
var cacheCount = BotInstance.GuildCache.Count;
|
Log($"Currently in {count} guilds.");
|
||||||
Log($"Currently in {count} guilds. Cached guild settings: {cacheCount}.");
|
|
||||||
await SendExternalStatistics(count);
|
await SendExternalStatistics(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,18 +14,6 @@ namespace BirthdayBot.BackgroundServices
|
||||||
{
|
{
|
||||||
var uptime = DateTimeOffset.UtcNow - Program.BotStartTime;
|
var uptime = DateTimeOffset.UtcNow - Program.BotStartTime;
|
||||||
Log($"Bot uptime: {Common.BotUptime}");
|
Log($"Bot uptime: {Common.BotUptime}");
|
||||||
|
|
||||||
// Disconnection warn
|
|
||||||
foreach (var shard in BotInstance.DiscordClient.Shards)
|
|
||||||
{
|
|
||||||
if (shard.ConnectionState == Discord.ConnectionState.Disconnected)
|
|
||||||
{
|
|
||||||
Log($"Shard {shard.ShardId} is disconnected! Restart the app if this persists.");
|
|
||||||
// The library alone cannot be restarted as it is in an unknown state. It was not designed to be restarted.
|
|
||||||
// TODO This is the part where we'd signal something to restart us if we were fancy.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,75 +17,68 @@ namespace BirthdayBot.BackgroundServices
|
||||||
{
|
{
|
||||||
// Build a list of all values to update
|
// Build a list of all values to update
|
||||||
var updateList = new Dictionary<ulong, List<ulong>>();
|
var updateList = new Dictionary<ulong, List<ulong>>();
|
||||||
foreach (var gi in BotInstance.GuildCache)
|
foreach (var g in BotInstance.DiscordClient.Guilds)
|
||||||
{
|
{
|
||||||
var guild = BotInstance.DiscordClient.GetGuild(gi.Key);
|
// Get list of IDs for all users who exist in the database and currently exist in the guild
|
||||||
if (guild == null) continue; // Have cache without being in guild. Unlikely, but...
|
var savedUserIds = from cu in await GuildUserConfiguration.LoadAllAsync(g.Id) select cu.UserId;
|
||||||
|
var guildUserIds = from gu in g.Users select gu.Id;
|
||||||
// Get IDs of cached users which are currently in the guild
|
var existingCachedIds = savedUserIds.Intersect(guildUserIds);
|
||||||
var cachedUserIds = from cu in gi.Value.Users select cu.UserId;
|
updateList[g.Id] = new List<ulong>(existingCachedIds);
|
||||||
var guildUserIds = from gu in guild.Users select gu.Id;
|
|
||||||
var existingCachedIds = cachedUserIds.Intersect(guildUserIds);
|
|
||||||
updateList[gi.Key] = new List<ulong>(existingCachedIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var db = await BotInstance.Config.DatabaseSettings.OpenConnectionAsync())
|
using var db = await Database.OpenConnectionAsync();
|
||||||
{
|
|
||||||
// Prepare to update a lot of last-seen values
|
// Statement for updating last_seen in guilds
|
||||||
var cUpdateGuild = db.CreateCommand();
|
var cUpdateGuild = db.CreateCommand();
|
||||||
cUpdateGuild.CommandText = $"update {GuildStateInformation.BackingTable} set last_seen = now() "
|
cUpdateGuild.CommandText = $"update {GuildConfiguration.BackingTable} set last_seen = now() "
|
||||||
+ "where guild_id = @Gid";
|
+ "where guild_id = @Gid";
|
||||||
var pUpdateG = cUpdateGuild.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
var pUpdateG = cUpdateGuild.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||||
cUpdateGuild.Prepare();
|
cUpdateGuild.Prepare();
|
||||||
|
|
||||||
|
// Statement for updating last_seen in guild users
|
||||||
var cUpdateGuildUser = db.CreateCommand();
|
var cUpdateGuildUser = db.CreateCommand();
|
||||||
cUpdateGuildUser.CommandText = $"update {GuildUserSettings.BackingTable} set last_seen = now() "
|
cUpdateGuildUser.CommandText = $"update {GuildUserConfiguration.BackingTable} set last_seen = now() "
|
||||||
+ "where guild_id = @Gid and user_id = @Uid";
|
+ "where guild_id = @Gid and user_id = @Uid";
|
||||||
var pUpdateGU_g = cUpdateGuildUser.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
var pUpdateGU_g = cUpdateGuildUser.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||||
var pUpdateGU_u = cUpdateGuildUser.Parameters.Add("@Uid", NpgsqlDbType.Bigint);
|
var pUpdateGU_u = cUpdateGuildUser.Parameters.Add("@Uid", NpgsqlDbType.Bigint);
|
||||||
cUpdateGuildUser.Prepare();
|
cUpdateGuildUser.Prepare();
|
||||||
|
|
||||||
|
// Do actual updates
|
||||||
int updatedGuilds = 0;
|
int updatedGuilds = 0;
|
||||||
int updatedUsers = 0;
|
int updatedUsers = 0;
|
||||||
|
|
||||||
// Do actual updates
|
|
||||||
foreach (var item in updateList)
|
foreach (var item in updateList)
|
||||||
{
|
{
|
||||||
var guild = item.Key;
|
var guild = item.Key;
|
||||||
var userlist = item.Value;
|
var userlist = item.Value;
|
||||||
|
|
||||||
pUpdateG.Value = (long)guild;
|
pUpdateG.Value = (long)guild;
|
||||||
updatedGuilds += cUpdateGuild.ExecuteNonQuery();
|
updatedGuilds += await cUpdateGuild.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
pUpdateGU_g.Value = (long)guild;
|
pUpdateGU_g.Value = (long)guild;
|
||||||
foreach (var userid in userlist)
|
foreach (var userid in userlist)
|
||||||
{
|
{
|
||||||
pUpdateGU_u.Value = (long)userid;
|
pUpdateGU_u.Value = (long)userid;
|
||||||
updatedUsers += cUpdateGuildUser.ExecuteNonQuery();
|
updatedUsers += await cUpdateGuildUser.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log($"Updated last-seen records: {updatedGuilds} guilds, {updatedUsers} users");
|
|
||||||
|
|
||||||
// Delete all old values - expects referencing tables to have 'on delete cascade'
|
// Delete all old values - expects referencing tables to have 'on delete cascade'
|
||||||
using (var t = db.BeginTransaction())
|
using var t = db.BeginTransaction();
|
||||||
{
|
|
||||||
int staleGuilds, staleUsers;
|
int staleGuilds, staleUsers;
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
// Delete data for guilds not seen in 4 weeks
|
// Delete data for guilds not seen in 4 weeks
|
||||||
c.CommandText = $"delete from {GuildStateInformation.BackingTable} where (now() - interval '28 days') > last_seen";
|
c.CommandText = $"delete from {GuildConfiguration.BackingTable} where (now() - interval '28 days') > last_seen";
|
||||||
staleGuilds = c.ExecuteNonQuery();
|
staleGuilds = c.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
// Delete data for users not seen in 8 weeks
|
// Delete data for users not seen in 8 weeks
|
||||||
c.CommandText = $"delete from {GuildUserSettings.BackingTable} where (now() - interval '56 days') > last_seen";
|
c.CommandText = $"delete from {GuildUserConfiguration.BackingTable} where (now() - interval '56 days') > last_seen";
|
||||||
staleUsers = c.ExecuteNonQuery();
|
staleUsers = c.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
Log($"Will remove {staleGuilds} guilds, {staleUsers} users.");
|
Log($"Will remove {staleGuilds} guilds, {staleUsers} users.");
|
||||||
t.Commit();
|
t.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ using Discord.Net;
|
||||||
using Discord.Webhook;
|
using Discord.Webhook;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using static BirthdayBot.UserInterface.CommandsCommon;
|
using static BirthdayBot.UserInterface.CommandsCommon;
|
||||||
|
@ -20,20 +19,20 @@ namespace BirthdayBot
|
||||||
private readonly HelpInfoCommands _cmdsHelp;
|
private readonly HelpInfoCommands _cmdsHelp;
|
||||||
private readonly ManagerCommands _cmdsMods;
|
private readonly ManagerCommands _cmdsMods;
|
||||||
|
|
||||||
private BackgroundServiceRunner _worker;
|
private readonly BackgroundServiceRunner _worker;
|
||||||
|
|
||||||
internal Configuration Config { get; }
|
internal Configuration Config { get; }
|
||||||
internal DiscordShardedClient DiscordClient { get; }
|
internal DiscordShardedClient DiscordClient { get; }
|
||||||
// TODO consider removal of the guild cache
|
|
||||||
internal ConcurrentDictionary<ulong, GuildStateInformation> GuildCache { get; }
|
|
||||||
internal DiscordWebhookClient LogWebhook { get; }
|
internal DiscordWebhookClient LogWebhook { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the bot connection and all its event handlers
|
||||||
|
/// </summary>
|
||||||
public BirthdayBot(Configuration conf, DiscordShardedClient dc)
|
public BirthdayBot(Configuration conf, DiscordShardedClient dc)
|
||||||
{
|
{
|
||||||
Config = conf;
|
Config = conf;
|
||||||
DiscordClient = dc;
|
DiscordClient = dc;
|
||||||
LogWebhook = new DiscordWebhookClient(conf.LogWebhook);
|
LogWebhook = new DiscordWebhookClient(conf.LogWebhook);
|
||||||
GuildCache = new ConcurrentDictionary<ulong, GuildStateInformation>();
|
|
||||||
|
|
||||||
_worker = new BackgroundServiceRunner(this);
|
_worker = new BackgroundServiceRunner(this);
|
||||||
|
|
||||||
|
@ -49,15 +48,17 @@ namespace BirthdayBot
|
||||||
foreach (var item in _cmdsMods.Commands) _dispatchCommands.Add(item.Item1, item.Item2);
|
foreach (var item in _cmdsMods.Commands) _dispatchCommands.Add(item.Item1, item.Item2);
|
||||||
|
|
||||||
// Register event handlers
|
// Register event handlers
|
||||||
DiscordClient.JoinedGuild += LoadGuild;
|
|
||||||
DiscordClient.GuildAvailable += LoadGuild;
|
|
||||||
DiscordClient.LeftGuild += DiscardGuild;
|
|
||||||
DiscordClient.ShardConnected += SetStatus;
|
DiscordClient.ShardConnected += SetStatus;
|
||||||
DiscordClient.MessageReceived += Dispatch;
|
DiscordClient.MessageReceived += Dispatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does some more basic initialization and then connects to Discord
|
||||||
|
/// </summary>
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
|
await Database.DoInitialDatabaseSetupAsync();
|
||||||
|
|
||||||
await DiscordClient.LoginAsync(TokenType.Bot, Config.BotToken);
|
await DiscordClient.LoginAsync(TokenType.Bot, Config.BotToken);
|
||||||
await DiscordClient.StartAsync();
|
await DiscordClient.StartAsync();
|
||||||
|
|
||||||
|
@ -76,21 +77,6 @@ namespace BirthdayBot
|
||||||
DiscordClient.Dispose();
|
DiscordClient.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadGuild(SocketGuild g)
|
|
||||||
{
|
|
||||||
if (!GuildCache.ContainsKey(g.Id))
|
|
||||||
{
|
|
||||||
var gi = await GuildStateInformation.LoadSettingsAsync(Config.DatabaseSettings, g.Id);
|
|
||||||
GuildCache.TryAdd(g.Id, gi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task DiscardGuild(SocketGuild g)
|
|
||||||
{
|
|
||||||
GuildCache.TryRemove(g.Id, out _);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetStatus(DiscordSocketClient shard) => await shard.SetGameAsync(CommandPrefix + "help");
|
private async Task SetStatus(DiscordSocketClient shard) => await shard.SetGameAsync(CommandPrefix + "help");
|
||||||
|
|
||||||
public async Task PushErrorLog(string source, string message)
|
public async Task PushErrorLog(string source, string message)
|
||||||
|
@ -114,9 +100,10 @@ namespace BirthdayBot
|
||||||
|
|
||||||
private async Task Dispatch(SocketMessage msg)
|
private async Task Dispatch(SocketMessage msg)
|
||||||
{
|
{
|
||||||
if (msg.Channel is IDMChannel) return;
|
if (!(msg.Channel is SocketTextChannel channel)) return;
|
||||||
if (msg.Author.IsBot) return;
|
if (msg.Author.IsBot || msg.Author.IsWebhook) return;
|
||||||
// TODO determine message type (pin, join, etc)
|
if (((IMessage)msg).Type != MessageType.Default) return;
|
||||||
|
var author = (SocketGuildUser)msg.Author;
|
||||||
|
|
||||||
// Limit 3:
|
// Limit 3:
|
||||||
// For all cases: base command, 2 parameters.
|
// For all cases: base command, 2 parameters.
|
||||||
|
@ -124,27 +111,23 @@ namespace BirthdayBot
|
||||||
var csplit = msg.Content.Split(" ", 3, StringSplitOptions.RemoveEmptyEntries);
|
var csplit = msg.Content.Split(" ", 3, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (csplit.Length > 0 && csplit[0].StartsWith(CommandPrefix, StringComparison.OrdinalIgnoreCase))
|
if (csplit.Length > 0 && csplit[0].StartsWith(CommandPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var channel = (SocketTextChannel)msg.Channel;
|
|
||||||
var author = (SocketGuildUser)msg.Author;
|
|
||||||
|
|
||||||
// Determine if it's something we're listening for.
|
// Determine if it's something we're listening for.
|
||||||
// Doing this first before the block check because a block check triggers a database query.
|
if (!_dispatchCommands.TryGetValue(csplit[0].Substring(CommandPrefix.Length), out CommandHandler command)) return;
|
||||||
CommandHandler command = null;
|
|
||||||
if (!_dispatchCommands.TryGetValue(csplit[0].Substring(CommandPrefix.Length), out command)) return;
|
// Load guild information here
|
||||||
|
var gconf = await GuildConfiguration.LoadAsync(channel.Guild.Id, false);
|
||||||
|
|
||||||
// Ban check
|
// Ban check
|
||||||
var gi = GuildCache[channel.Guild.Id];
|
if (!gconf.IsBotModerator(author)) // skip check if user is a moderator
|
||||||
// Skip ban check if user is a manager
|
|
||||||
if (!gi.IsUserModerator(author))
|
|
||||||
{
|
{
|
||||||
if (gi.IsUserBlockedAsync(author.Id).GetAwaiter().GetResult()) return;
|
if (await gconf.IsUserBlockedAsync(author.Id)) return; // silently ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command
|
// Execute the command
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Program.Log("Command", $"{channel.Guild.Name}/{author.Username}#{author.Discriminator}: {msg.Content}");
|
Program.Log("Command", $"{channel.Guild.Name}/{author.Username}#{author.Discriminator}: {msg.Content}");
|
||||||
await command(csplit, channel, author);
|
await command(csplit, gconf, channel, author);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<Version>2.2.1</Version>
|
<Version>2.3.1</Version>
|
||||||
<PackageId>BirthdayBot</PackageId>
|
<PackageId>BirthdayBot</PackageId>
|
||||||
<Authors>NoiTheCat</Authors>
|
<Authors>NoiTheCat</Authors>
|
||||||
<Product>BirthdayBot</Product>
|
<Product>BirthdayBot</Product>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<PackageReference Include="Discord.Net" Version="2.2.0" />
|
<PackageReference Include="Discord.Net" Version="2.2.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="NodaTime" Version="3.0.0" />
|
<PackageReference Include="NodaTime" Version="3.0.0" />
|
||||||
<PackageReference Include="Npgsql" Version="4.1.3.1" />
|
<PackageReference Include="Npgsql" Version="4.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -14,7 +14,6 @@ namespace BirthdayBot
|
||||||
public string BotToken { get; }
|
public string BotToken { get; }
|
||||||
public string LogWebhook { get; }
|
public string LogWebhook { get; }
|
||||||
public string DBotsToken { get; }
|
public string DBotsToken { get; }
|
||||||
public Database DatabaseSettings { get; }
|
|
||||||
public int ShardCount { get; }
|
public int ShardCount { get; }
|
||||||
|
|
||||||
public Configuration()
|
public Configuration()
|
||||||
|
@ -52,7 +51,7 @@ namespace BirthdayBot
|
||||||
var sqlcs = jc["SqlConnectionString"]?.Value<string>();
|
var sqlcs = jc["SqlConnectionString"]?.Value<string>();
|
||||||
if (string.IsNullOrWhiteSpace(sqlcs))
|
if (string.IsNullOrWhiteSpace(sqlcs))
|
||||||
throw new Exception("'SqlConnectionString' must be specified.");
|
throw new Exception("'SqlConnectionString' must be specified.");
|
||||||
DatabaseSettings = new Database(sqlcs);
|
Database.DBConnectionString = sqlcs;
|
||||||
|
|
||||||
int? sc = jc["ShardCount"]?.Value<int>();
|
int? sc = jc["ShardCount"]?.Value<int>();
|
||||||
if (!sc.HasValue) ShardCount = 1;
|
if (!sc.HasValue) ShardCount = 1;
|
||||||
|
|
|
@ -1,42 +1,36 @@
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BirthdayBot.Data
|
namespace BirthdayBot.Data
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Some database abstractions.
|
/// Database access and some abstractions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class Database
|
internal static class Database
|
||||||
{
|
{
|
||||||
/*
|
private static string _connString;
|
||||||
* Database storage in this project, explained:
|
public static string DBConnectionString
|
||||||
* Each guild gets a row in the settings table. This table is referred to when doing most things.
|
|
||||||
* Within each guild, each known user gets a row in the users table with specific information specified.
|
|
||||||
* Users can override certain settings in global, such as time zone.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private string DBConnectionString { get; }
|
|
||||||
|
|
||||||
public Database(string connString)
|
|
||||||
{
|
{
|
||||||
DBConnectionString = connString;
|
get => _connString;
|
||||||
|
set => _connString = "Minimum Pool Size=5;Maximum Pool Size=50;Connection Idle Lifetime=30;" + value;
|
||||||
// Database initialization happens here as well.
|
|
||||||
SetupTables();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NpgsqlConnection> OpenConnectionAsync()
|
public static async Task<NpgsqlConnection> OpenConnectionAsync()
|
||||||
{
|
{
|
||||||
|
if (DBConnectionString == null) throw new Exception("Database connection string not set");
|
||||||
var db = new NpgsqlConnection(DBConnectionString);
|
var db = new NpgsqlConnection(DBConnectionString);
|
||||||
await db.OpenAsync();
|
await db.OpenAsync();
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupTables()
|
public static async Task DoInitialDatabaseSetupAsync()
|
||||||
{
|
{
|
||||||
using var db = OpenConnectionAsync().GetAwaiter().GetResult();
|
using var db = await OpenConnectionAsync();
|
||||||
GuildStateInformation.SetUpDatabaseTable(db); // Note: Call this first. (Foreign reference constraints.)
|
|
||||||
GuildUserSettings.SetUpDatabaseTable(db);
|
// Refer to the methods being called for information on how the database is set up.
|
||||||
|
await GuildConfiguration.DatabaseSetupAsync(db); // Note: Call this first. (Foreign reference constraints.)
|
||||||
|
await GuildUserConfiguration.DatabaseSetupAsync(db);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
263
Data/GuildConfiguration.cs
Normal file
263
Data/GuildConfiguration.cs
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Npgsql;
|
||||||
|
using NpgsqlTypes;
|
||||||
|
using System;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents guild-specific configuration as exists in the database.
|
||||||
|
/// Updating any property requires a call to <see cref="UpdateAsync"/> for changes to take effect.
|
||||||
|
/// </summary>
|
||||||
|
class GuildConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets this configuration's corresponding guild ID.
|
||||||
|
/// </summary>
|
||||||
|
public ulong GuildId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the guild's designated usable role ID.
|
||||||
|
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ulong? RoleId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the announcement channel ID.
|
||||||
|
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ulong? AnnounceChannelId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the guild's default time zone ztring.
|
||||||
|
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string TimeZone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the guild's moderated mode setting.
|
||||||
|
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsModerated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the guild's corresponding bot moderator role ID.
|
||||||
|
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ulong? ModeratorRole { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the guild-specific birthday announcement message.
|
||||||
|
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public (string, string) AnnounceMessages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the announcement ping setting.
|
||||||
|
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool AnnouncePing { get; set; }
|
||||||
|
|
||||||
|
// Called by Load. Double-check ordinals when changes are made.
|
||||||
|
private GuildConfiguration(DbDataReader reader)
|
||||||
|
{
|
||||||
|
GuildId = (ulong)reader.GetInt64(0);
|
||||||
|
if (!reader.IsDBNull(1)) RoleId = (ulong)reader.GetInt64(1);
|
||||||
|
if (!reader.IsDBNull(2)) AnnounceChannelId = (ulong)reader.GetInt64(2);
|
||||||
|
TimeZone = reader.IsDBNull(3) ? null : reader.GetString(3);
|
||||||
|
IsModerated = reader.GetBoolean(4);
|
||||||
|
if (!reader.IsDBNull(5)) ModeratorRole = (ulong)reader.GetInt64(5);
|
||||||
|
string announceMsg = reader.IsDBNull(6) ? null : reader.GetString(6);
|
||||||
|
string announceMsgPl = reader.IsDBNull(7) ? null : reader.GetString(7);
|
||||||
|
AnnounceMessages = (announceMsg, announceMsgPl);
|
||||||
|
AnnouncePing = reader.GetBoolean(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the given user exists in the block list.
|
||||||
|
/// If the server is in moderated mode, this always returns true.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> IsUserBlockedAsync(ulong userId)
|
||||||
|
{
|
||||||
|
if (IsModerated) return true;
|
||||||
|
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"select * from {BackingTableBans} "
|
||||||
|
+ "where guild_id = @Gid and user_id = @Uid";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
||||||
|
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
|
||||||
|
c.Prepare();
|
||||||
|
using var r = await c.ExecuteReaderAsync();
|
||||||
|
if (await r.ReadAsync()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified user to the block list corresponding to this guild.
|
||||||
|
/// </summary>
|
||||||
|
public async Task BlockUserAsync(ulong userId)
|
||||||
|
{
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"insert into {BackingTableBans} (guild_id, user_id) "
|
||||||
|
+ "values (@Gid, @Uid) "
|
||||||
|
+ "on conflict (guild_id, user_id) do nothing";
|
||||||
|
// There is no validation on whether the requested user is even in the guild. will this be a problem?
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
||||||
|
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
|
||||||
|
c.Prepare();
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the specified user from the block list corresponding to this guild.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if a user has been removed, false if the requested user was not in this list.</returns>
|
||||||
|
public async Task<bool> UnblockUserAsync(ulong userId)
|
||||||
|
{
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"delete from {BackingTableBans} where "
|
||||||
|
+ "guild_id = @Gid and user_id = @Uid";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
||||||
|
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
|
||||||
|
c.Prepare();
|
||||||
|
var result = await c.ExecuteNonQueryAsync();
|
||||||
|
return result != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the given user can be considered a bot moderator.
|
||||||
|
/// Checks for either the Manage Guild permission or if the user is within a predetermined role.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsBotModerator(SocketGuildUser user)
|
||||||
|
=> user.GuildPermissions.ManageGuild || (ModeratorRole.HasValue && user.Roles.Any(r => r.Id == ModeratorRole.Value));
|
||||||
|
|
||||||
|
#region Database
|
||||||
|
public const string BackingTable = "settings";
|
||||||
|
public const string BackingTableBans = "banned_users";
|
||||||
|
|
||||||
|
internal static async Task DatabaseSetupAsync(NpgsqlConnection db)
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = $"create table if not exists {BackingTable} ("
|
||||||
|
+ "guild_id bigint primary key, "
|
||||||
|
+ "role_id bigint null, "
|
||||||
|
+ "channel_announce_id bigint null, "
|
||||||
|
+ "time_zone text null, "
|
||||||
|
+ "moderated boolean not null default FALSE, "
|
||||||
|
+ "moderator_role bigint null, "
|
||||||
|
+ "announce_message text null, "
|
||||||
|
+ "announce_message_pl text null, "
|
||||||
|
+ "announce_ping boolean not null default FALSE, "
|
||||||
|
+ "last_seen timestamptz not null default NOW()"
|
||||||
|
+ ")";
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = $"create table if not exists {BackingTableBans} ("
|
||||||
|
+ $"guild_id bigint not null references {BackingTable} ON DELETE CASCADE, "
|
||||||
|
+ "user_id bigint not null, "
|
||||||
|
+ "PRIMARY KEY (guild_id, user_id)"
|
||||||
|
+ ")";
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches guild settings from the database. If no corresponding entry exists, it will be created.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nullIfUnknown">
|
||||||
|
/// If true, this method shall not create a new entry and will return null if the guild does
|
||||||
|
/// not exist in the database.
|
||||||
|
/// </param>
|
||||||
|
public static async Task<GuildConfiguration> LoadAsync(ulong guildId, bool nullIfUnknown)
|
||||||
|
{
|
||||||
|
using (var db = await Database.OpenConnectionAsync())
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
// Take note of ordinals for the constructor
|
||||||
|
c.CommandText = "select guild_id, role_id, channel_announce_id, time_zone, "
|
||||||
|
+ " moderated, moderator_role, announce_message, announce_message_pl, announce_ping "
|
||||||
|
+ $"from {BackingTable} where guild_id = @Gid";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
||||||
|
c.Prepare();
|
||||||
|
using var r = await c.ExecuteReaderAsync();
|
||||||
|
if (await r.ReadAsync()) return new GuildConfiguration(r);
|
||||||
|
}
|
||||||
|
if (nullIfUnknown) return null;
|
||||||
|
|
||||||
|
// If we got here, no row exists. Create it with default values.
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = $"insert into {BackingTable} (guild_id) values (@Gid)";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
||||||
|
c.Prepare();
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// With a new row created, try this again
|
||||||
|
return await LoadAsync(guildId, nullIfUnknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates values on the backing database with values from this object instance.
|
||||||
|
/// </summary>
|
||||||
|
public async Task UpdateAsync()
|
||||||
|
{
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"update {BackingTable} set "
|
||||||
|
+ "role_id = @RoleId, "
|
||||||
|
+ "channel_announce_id = @ChannelId, "
|
||||||
|
+ "time_zone = @TimeZone, "
|
||||||
|
+ "moderated = @Moderated, "
|
||||||
|
+ "moderator_role = @ModRole, "
|
||||||
|
+ "announce_message = @AnnounceMsg, "
|
||||||
|
+ "announce_message_pl = @AnnounceMsgPl, "
|
||||||
|
+ "announce_ping = @AnnouncePing "
|
||||||
|
+ "where guild_id = @Gid";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
||||||
|
NpgsqlParameter p;
|
||||||
|
|
||||||
|
p = c.Parameters.Add("@RoleId", NpgsqlDbType.Bigint);
|
||||||
|
if (RoleId.HasValue) p.Value = (long)RoleId.Value;
|
||||||
|
else p.Value = DBNull.Value;
|
||||||
|
|
||||||
|
p = c.Parameters.Add("@ChannelId", NpgsqlDbType.Bigint);
|
||||||
|
if (AnnounceChannelId.HasValue) p.Value = (long)AnnounceChannelId.Value;
|
||||||
|
else p.Value = DBNull.Value;
|
||||||
|
|
||||||
|
p = c.Parameters.Add("@TimeZone", NpgsqlDbType.Text);
|
||||||
|
if (TimeZone != null) p.Value = TimeZone;
|
||||||
|
else p.Value = DBNull.Value;
|
||||||
|
|
||||||
|
c.Parameters.Add("@Moderated", NpgsqlDbType.Boolean).Value = IsModerated;
|
||||||
|
|
||||||
|
p = c.Parameters.Add("@ModRole", NpgsqlDbType.Bigint);
|
||||||
|
if (ModeratorRole.HasValue) p.Value = (long)ModeratorRole.Value;
|
||||||
|
else p.Value = DBNull.Value;
|
||||||
|
|
||||||
|
p = c.Parameters.Add("@AnnounceMsg", NpgsqlDbType.Text);
|
||||||
|
if (AnnounceMessages.Item1 != null) p.Value = AnnounceMessages.Item1;
|
||||||
|
else p.Value = DBNull.Value;
|
||||||
|
|
||||||
|
p = c.Parameters.Add("@AnnounceMsgPl", NpgsqlDbType.Text);
|
||||||
|
if (AnnounceMessages.Item2 != null) p.Value = AnnounceMessages.Item2;
|
||||||
|
else p.Value = DBNull.Value;
|
||||||
|
|
||||||
|
c.Parameters.Add("@AnnouncePing", NpgsqlDbType.Boolean).Value = AnnouncePing;
|
||||||
|
|
||||||
|
c.Prepare();
|
||||||
|
c.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,429 +0,0 @@
|
||||||
using Discord.WebSocket;
|
|
||||||
using Npgsql;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data.Common;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.Data
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 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>
|
|
||||||
class GuildStateInformation
|
|
||||||
{
|
|
||||||
private readonly Database _db;
|
|
||||||
private ulong? _bdayRole;
|
|
||||||
private ulong? _announceCh;
|
|
||||||
private ulong? _modRole;
|
|
||||||
private string _tz;
|
|
||||||
private bool _moderated;
|
|
||||||
private string _announceMsg;
|
|
||||||
private string _announceMsgPl;
|
|
||||||
private bool _announcePing;
|
|
||||||
private readonly Dictionary<ulong, GuildUserSettings> _userCache;
|
|
||||||
|
|
||||||
public ulong GuildId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of cached registered user information.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<GuildUserSettings> Users {
|
|
||||||
get {
|
|
||||||
var items = new List<GuildUserSettings>();
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
foreach (var item in _userCache.Values) items.Add(item);
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the guild's designated Role ID.
|
|
||||||
/// </summary>
|
|
||||||
public ulong? RoleId { get { lock (this) { return _bdayRole; } } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the designated announcement Channel ID.
|
|
||||||
/// </summary>
|
|
||||||
public ulong? AnnounceChannelId { get { lock (this) { return _announceCh; } } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the guild's default time zone.
|
|
||||||
/// </summary>
|
|
||||||
public string TimeZone { get { lock (this) { return _tz; } } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets whether the guild is in moderated mode.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsModerated { get { lock (this) { return _moderated; } } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the designated moderator role ID.
|
|
||||||
/// </summary>
|
|
||||||
public ulong? ModeratorRole { get { lock (this) { return _modRole; } } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the guild-specific birthday announcement message.
|
|
||||||
/// </summary>
|
|
||||||
public (string, string) AnnounceMessages { get { lock (this) { return (_announceMsg, _announceMsgPl); } } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets whether to ping users in the announcement message instead of displaying their names.
|
|
||||||
/// </summary>
|
|
||||||
public bool AnnouncePing { get { lock (this) { return _announcePing; } } }
|
|
||||||
|
|
||||||
// Called by LoadSettingsAsync. Double-check ordinals when changes are made.
|
|
||||||
private GuildStateInformation(DbDataReader reader, Database dbconfig)
|
|
||||||
{
|
|
||||||
_db = dbconfig;
|
|
||||||
|
|
||||||
GuildId = (ulong)reader.GetInt64(0);
|
|
||||||
if (!reader.IsDBNull(1))
|
|
||||||
{
|
|
||||||
_bdayRole = (ulong)reader.GetInt64(1);
|
|
||||||
}
|
|
||||||
if (!reader.IsDBNull(2)) _announceCh = (ulong)reader.GetInt64(2);
|
|
||||||
_tz = reader.IsDBNull(3) ? null : reader.GetString(3);
|
|
||||||
_moderated = reader.GetBoolean(4);
|
|
||||||
if (!reader.IsDBNull(5)) _modRole = (ulong)reader.GetInt64(5);
|
|
||||||
_announceMsg = reader.IsDBNull(6) ? null : reader.GetString(6);
|
|
||||||
_announceMsgPl = reader.IsDBNull(7) ? null : reader.GetString(7);
|
|
||||||
_announcePing = reader.GetBoolean(8);
|
|
||||||
|
|
||||||
// Get user information loaded up.
|
|
||||||
var userresult = GuildUserSettings.GetGuildUsersAsync(dbconfig, GuildId);
|
|
||||||
_userCache = new Dictionary<ulong, GuildUserSettings>();
|
|
||||||
foreach (var item in userresult)
|
|
||||||
{
|
|
||||||
_userCache.Add(item.UserId, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets user information from th is guild. If the user doesn't exist in the backing database,
|
|
||||||
/// a new instance is created which is capable of adding the user to the database.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// For users with the Known property set to false, be sure to call
|
|
||||||
/// <see cref="GuildUserSettings.DeleteAsync(Database)"/> if the resulting object is otherwise unused.
|
|
||||||
/// </remarks>
|
|
||||||
public GuildUserSettings GetUser(ulong userId)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
if (_userCache.ContainsKey(userId))
|
|
||||||
{
|
|
||||||
return _userCache[userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// No result. Create a blank entry and add it to the list,
|
|
||||||
// in case it gets updated and then referenced later.
|
|
||||||
var blank = new GuildUserSettings(GuildId, userId);
|
|
||||||
_userCache.Add(userId, blank);
|
|
||||||
return blank;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the user from the backing database. Drops the locally cached entry.
|
|
||||||
/// </summary>
|
|
||||||
public async Task DeleteUserAsync(ulong userId)
|
|
||||||
{
|
|
||||||
GuildUserSettings user = null;
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
if (!_userCache.TryGetValue(userId, out user))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_userCache.Remove(userId);
|
|
||||||
}
|
|
||||||
await user.DeleteAsync(_db);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> IsUserBlockedAsync(ulong userId)
|
|
||||||
{
|
|
||||||
if (IsModerated) return true;
|
|
||||||
|
|
||||||
// Block list is not cached, thus doing a database lookup
|
|
||||||
// TODO cache block list?
|
|
||||||
using (var db = await _db.OpenConnectionAsync())
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"select * from {BackingTableBans} "
|
|
||||||
+ "where guild_id = @Gid and user_id = @Uid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
|
|
||||||
c.Prepare();
|
|
||||||
using (var r = await c.ExecuteReaderAsync())
|
|
||||||
{
|
|
||||||
if (await r.ReadAsync()) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the given user is a moderator either by having the Manage Server permission or
|
|
||||||
/// being in the designated moderator role.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsUserModerator(SocketGuildUser user)
|
|
||||||
{
|
|
||||||
if (user.GuildPermissions.ManageGuild) return true;
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
if (ModeratorRole.HasValue)
|
|
||||||
{
|
|
||||||
if (user.Roles.Where(r => r.Id == ModeratorRole.Value).Count() > 0) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds the specified user to the block list, preventing them from issuing commands.
|
|
||||||
/// </summary>
|
|
||||||
public async Task BlockUserAsync(ulong userId)
|
|
||||||
{
|
|
||||||
// TODO cache block list?
|
|
||||||
using (var db = await _db.OpenConnectionAsync())
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"insert into {BackingTableBans} (guild_id, user_id) "
|
|
||||||
+ "values (@Gid, @Uid) "
|
|
||||||
+ "on conflict (guild_id, user_id) do nothing";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
|
|
||||||
c.Prepare();
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnbanUserAsync(ulong userId)
|
|
||||||
{
|
|
||||||
// TODO cache block list?
|
|
||||||
using (var db = await _db.OpenConnectionAsync())
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"delete from {BackingTableBans} where "
|
|
||||||
+ "guild_id = @Gid and user_id = @Uid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
|
|
||||||
c.Prepare();
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateRole(ulong roleId)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
_bdayRole = roleId;
|
|
||||||
UpdateDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateAnnounceChannel(ulong? channelId)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
_announceCh = channelId;
|
|
||||||
UpdateDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateTimeZone(string tzString)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
_tz = tzString;
|
|
||||||
UpdateDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateModeratedMode(bool isModerated)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
_moderated = isModerated;
|
|
||||||
UpdateDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateModeratorRole(ulong? roleId)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
_modRole = roleId;
|
|
||||||
UpdateDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateAnnounceMessage(string message, bool plural)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
if (plural) _announceMsgPl = message;
|
|
||||||
else _announceMsg = message;
|
|
||||||
|
|
||||||
UpdateDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateAnnouncePing(bool value)
|
|
||||||
{
|
|
||||||
lock (this)
|
|
||||||
{
|
|
||||||
_announcePing = value;
|
|
||||||
UpdateDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Database
|
|
||||||
public const string BackingTable = "settings";
|
|
||||||
public const string BackingTableBans = "banned_users";
|
|
||||||
|
|
||||||
internal static void SetUpDatabaseTable(NpgsqlConnection db)
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"create table if not exists {BackingTable} ("
|
|
||||||
+ "guild_id bigint primary key, "
|
|
||||||
+ "role_id bigint null, "
|
|
||||||
+ "channel_announce_id bigint null, "
|
|
||||||
+ "time_zone text null, "
|
|
||||||
+ "moderated boolean not null default FALSE, "
|
|
||||||
+ "moderator_role bigint null, "
|
|
||||||
+ "announce_message text null, "
|
|
||||||
+ "announce_message_pl text null, "
|
|
||||||
+ "announce_ping boolean not null default FALSE, "
|
|
||||||
+ "last_seen timestamptz not null default NOW()"
|
|
||||||
+ ")";
|
|
||||||
c.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"create table if not exists {BackingTableBans} ("
|
|
||||||
+ $"guild_id bigint not null references {BackingTable} ON DELETE CASCADE, "
|
|
||||||
+ "user_id bigint not null, "
|
|
||||||
+ "PRIMARY KEY (guild_id, user_id)"
|
|
||||||
+ ")";
|
|
||||||
c.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves an object instance representative of guild settings for the specified guild.
|
|
||||||
/// If settings for the given guild do not yet exist, a new value is created.
|
|
||||||
/// </summary>
|
|
||||||
internal async static Task<GuildStateInformation> LoadSettingsAsync(Database dbsettings, ulong guild)
|
|
||||||
{
|
|
||||||
using (var db = await dbsettings.OpenConnectionAsync())
|
|
||||||
{
|
|
||||||
using (var 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, moderator_role, announce_message, announce_message_pl, announce_ping "
|
|
||||||
+ $"from {BackingTable} where guild_id = @Gid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guild;
|
|
||||||
c.Prepare();
|
|
||||||
using (var r = await c.ExecuteReaderAsync())
|
|
||||||
{
|
|
||||||
if (await r.ReadAsync())
|
|
||||||
{
|
|
||||||
return new GuildStateInformation(r, dbsettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got here, no row exists. Create it.
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"insert into {BackingTable} (guild_id) values (@Gid)";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guild;
|
|
||||||
c.Prepare();
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New row created. Try this again.
|
|
||||||
return await LoadSettingsAsync(dbsettings, guild);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the backing database with values from this instance
|
|
||||||
/// This is a non-asynchronous operation. That may be bad.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateDatabase()
|
|
||||||
{
|
|
||||||
using (var db = _db.OpenConnectionAsync().GetAwaiter().GetResult())
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"update {BackingTable} set "
|
|
||||||
+ "role_id = @RoleId, "
|
|
||||||
+ "channel_announce_id = @ChannelId, "
|
|
||||||
+ "time_zone = @TimeZone, "
|
|
||||||
+ "moderated = @Moderated, "
|
|
||||||
+ "moderator_role = @ModRole, "
|
|
||||||
+ "announce_message = @AnnounceMsg, "
|
|
||||||
+ "announce_message_pl = @AnnounceMsgPl, "
|
|
||||||
+ "announce_ping = @AnnouncePing "
|
|
||||||
+ "where guild_id = @Gid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
|
||||||
NpgsqlParameter p;
|
|
||||||
|
|
||||||
p = c.Parameters.Add("@RoleId", NpgsqlDbType.Bigint);
|
|
||||||
if (RoleId.HasValue) p.Value = (long)RoleId.Value;
|
|
||||||
else p.Value = DBNull.Value;
|
|
||||||
|
|
||||||
p = c.Parameters.Add("@ChannelId", NpgsqlDbType.Bigint);
|
|
||||||
if (_announceCh.HasValue) p.Value = (long)_announceCh.Value;
|
|
||||||
else p.Value = DBNull.Value;
|
|
||||||
|
|
||||||
p = c.Parameters.Add("@TimeZone", NpgsqlDbType.Text);
|
|
||||||
if (_tz != null) p.Value = _tz;
|
|
||||||
else p.Value = DBNull.Value;
|
|
||||||
|
|
||||||
c.Parameters.Add("@Moderated", NpgsqlDbType.Boolean).Value = _moderated;
|
|
||||||
|
|
||||||
p = c.Parameters.Add("@ModRole", NpgsqlDbType.Bigint);
|
|
||||||
if (ModeratorRole.HasValue) p.Value = (long)ModeratorRole.Value;
|
|
||||||
else p.Value = DBNull.Value;
|
|
||||||
|
|
||||||
p = c.Parameters.Add("@AnnounceMsg", NpgsqlDbType.Text);
|
|
||||||
if (_announceMsg != null) p.Value = _announceMsg;
|
|
||||||
else p.Value = DBNull.Value;
|
|
||||||
|
|
||||||
p = c.Parameters.Add("@AnnounceMsgPl", NpgsqlDbType.Text);
|
|
||||||
if (_announceMsgPl != null) p.Value = _announceMsgPl;
|
|
||||||
else p.Value = DBNull.Value;
|
|
||||||
|
|
||||||
c.Parameters.Add("@AnnouncePing", NpgsqlDbType.Boolean).Value = _announcePing;
|
|
||||||
|
|
||||||
c.Prepare();
|
|
||||||
c.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
151
Data/GuildUserConfiguration.cs
Normal file
151
Data/GuildUserConfiguration.cs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
using Npgsql;
|
||||||
|
using NpgsqlTypes;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents configuration for a guild user.
|
||||||
|
/// </summary>
|
||||||
|
class GuildUserConfiguration
|
||||||
|
{
|
||||||
|
public ulong GuildId { get; }
|
||||||
|
public ulong UserId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Month of birth as a numeric value. Range 1-12.
|
||||||
|
/// </summary>
|
||||||
|
public int BirthMonth { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Day of birth as a numeric value. Ranges between 1-31 or lower based on month value.
|
||||||
|
/// </summary>
|
||||||
|
public int BirthDay { get; private set; }
|
||||||
|
|
||||||
|
public string TimeZone { get; private set; }
|
||||||
|
public bool IsKnown { get { return BirthMonth != 0 && BirthDay != 0; } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new, data-less instance without a corresponding database entry.
|
||||||
|
/// Calling <see cref="UpdateAsync(int, int, int)"/> will create a real database enty
|
||||||
|
/// </summary>
|
||||||
|
private GuildUserConfiguration(ulong guildId, ulong userId)
|
||||||
|
{
|
||||||
|
GuildId = guildId;
|
||||||
|
UserId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by GetGuildUsersAsync. Double-check ordinals when changes are made.
|
||||||
|
private GuildUserConfiguration(DbDataReader reader)
|
||||||
|
{
|
||||||
|
GuildId = (ulong)reader.GetInt64(0);
|
||||||
|
UserId = (ulong)reader.GetInt64(1);
|
||||||
|
BirthMonth = reader.GetInt32(2);
|
||||||
|
BirthDay = reader.GetInt32(3);
|
||||||
|
if (!reader.IsDBNull(4)) TimeZone = reader.GetString(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates user with given information.
|
||||||
|
/// </summary>
|
||||||
|
public async Task UpdateAsync(int month, int day, string newtz)
|
||||||
|
{
|
||||||
|
using (var db = await Database.OpenConnectionAsync())
|
||||||
|
{
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"insert into {BackingTable} "
|
||||||
|
+ "(guild_id, user_id, birth_month, birth_day, time_zone) values "
|
||||||
|
+ "(@Gid, @Uid, @Month, @Day, @Tz) "
|
||||||
|
+ "on conflict (guild_id, user_id) do update "
|
||||||
|
+ "set birth_month = EXCLUDED.birth_month, birth_day = EXCLUDED.birth_day, time_zone = EXCLUDED.time_zone";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
||||||
|
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
|
||||||
|
c.Parameters.Add("@Month", NpgsqlDbType.Numeric).Value = month;
|
||||||
|
c.Parameters.Add("@Day", NpgsqlDbType.Numeric).Value = day;
|
||||||
|
var tzp = c.Parameters.Add("@Tz", NpgsqlDbType.Text);
|
||||||
|
if (newtz != null) tzp.Value = newtz;
|
||||||
|
else tzp.Value = DBNull.Value;
|
||||||
|
c.Prepare();
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database update succeeded; update instance values
|
||||||
|
BirthMonth = month;
|
||||||
|
BirthDay = day;
|
||||||
|
TimeZone = newtz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes information of this user from the backing database.
|
||||||
|
/// The corresponding object reference should ideally be discarded after calling this.
|
||||||
|
/// </summary>
|
||||||
|
public async Task DeleteAsync()
|
||||||
|
{
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"delete from {BackingTable} "
|
||||||
|
+ "where guild_id = @Gid and user_id = @Uid";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
||||||
|
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
|
||||||
|
c.Prepare();
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Database
|
||||||
|
public const string BackingTable = "user_birthdays";
|
||||||
|
// Take note of ordinals for use in the constructor
|
||||||
|
private const string SelectFields = "guild_id, user_id, birth_month, birth_day, time_zone";
|
||||||
|
|
||||||
|
internal static async Task DatabaseSetupAsync(NpgsqlConnection db)
|
||||||
|
{
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"create table if not exists {BackingTable} ("
|
||||||
|
+ $"guild_id bigint not null references {GuildConfiguration.BackingTable} ON DELETE CASCADE, "
|
||||||
|
+ "user_id bigint not null, "
|
||||||
|
+ "birth_month integer not null, "
|
||||||
|
+ "birth_day integer not null, "
|
||||||
|
+ "time_zone text null, "
|
||||||
|
+ "last_seen timestamptz not null default NOW(), "
|
||||||
|
+ "PRIMARY KEY (guild_id, user_id)" // index automatically created with this
|
||||||
|
+ ")";
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve a user's configuration. Returns a new, updateable instance if none is found.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<GuildUserConfiguration> LoadAsync(ulong guildId, ulong userId)
|
||||||
|
{
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"select {SelectFields} from {BackingTable} where guild_id = @Gid and user_id = @Uid";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
||||||
|
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
|
||||||
|
c.Prepare();
|
||||||
|
|
||||||
|
using var r = c.ExecuteReader();
|
||||||
|
if (await r.ReadAsync()) return new GuildUserConfiguration(r);
|
||||||
|
else return new GuildUserConfiguration(guildId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all known user configuration records associated with the specified guild.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<IEnumerable<GuildUserConfiguration>> LoadAllAsync(ulong guildId)
|
||||||
|
{
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $"select {SelectFields} from {BackingTable} where guild_id = @Gid";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
||||||
|
c.Prepare();
|
||||||
|
|
||||||
|
using var r = await c.ExecuteReaderAsync();
|
||||||
|
var result = new List<GuildUserConfiguration>();
|
||||||
|
while (await r.ReadAsync()) result.Add(new GuildUserConfiguration(r));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,169 +0,0 @@
|
||||||
using Npgsql;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data.Common;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.Data
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Representation of a user's birthday settings within a guild.
|
|
||||||
/// Instances are held and managed by <see cref="="GuildStateInformation"/>.
|
|
||||||
/// </summary>
|
|
||||||
class GuildUserSettings
|
|
||||||
{
|
|
||||||
private int _month;
|
|
||||||
private int _day;
|
|
||||||
private string _tz;
|
|
||||||
|
|
||||||
public ulong GuildId { get; }
|
|
||||||
public ulong UserId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Month of birth as a numeric value. Range 1-12.
|
|
||||||
/// </summary>
|
|
||||||
public int BirthMonth { get { return _month; } }
|
|
||||||
/// <summary>
|
|
||||||
/// Day of birth as a numeric value. Ranges between 1-31 or lower based on month value.
|
|
||||||
/// </summary>
|
|
||||||
public int BirthDay { get { return _day; } }
|
|
||||||
|
|
||||||
public string TimeZone { get { return _tz; } }
|
|
||||||
public bool IsKnown { get { return _month != 0 && _day != 0; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a data-less instance without any useful information.
|
|
||||||
/// Calling <see cref="UpdateAsync(int, int, int)"/> will create a real database enty
|
|
||||||
/// </summary>
|
|
||||||
public GuildUserSettings(ulong guildId, ulong userId)
|
|
||||||
{
|
|
||||||
GuildId = guildId;
|
|
||||||
UserId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by GetGuildUsersAsync. Double-check ordinals when changes are made.
|
|
||||||
private GuildUserSettings(DbDataReader reader)
|
|
||||||
{
|
|
||||||
GuildId = (ulong)reader.GetInt64(0);
|
|
||||||
UserId = (ulong)reader.GetInt64(1);
|
|
||||||
_month = reader.GetInt32(2);
|
|
||||||
_day = reader.GetInt32(3);
|
|
||||||
if (!reader.IsDBNull(4)) _tz = reader.GetString(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates user with given information.
|
|
||||||
/// NOTE: If there exists a tz value and the update contains none, the old tz value is retained.
|
|
||||||
/// </summary>
|
|
||||||
public async Task UpdateAsync(int month, int day, string newtz, Database dbconfig)
|
|
||||||
{
|
|
||||||
// TODO note from rewrite: huh? why are we doing this here?
|
|
||||||
var inserttz = newtz ?? TimeZone;
|
|
||||||
|
|
||||||
using (var db = await dbconfig.OpenConnectionAsync())
|
|
||||||
{
|
|
||||||
// Will do a delete/insert instead of insert...on conflict update. Because lazy.
|
|
||||||
using (var t = db.BeginTransaction())
|
|
||||||
{
|
|
||||||
await DoDeleteAsync(db);
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"insert into {BackingTable} "
|
|
||||||
+ "(guild_id, user_id, birth_month, birth_day, time_zone) values "
|
|
||||||
+ "(@Gid, @Uid, @Month, @Day, @Tz)";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
|
|
||||||
c.Parameters.Add("@Month", NpgsqlDbType.Numeric).Value = month;
|
|
||||||
c.Parameters.Add("@Day", NpgsqlDbType.Numeric).Value = day;
|
|
||||||
var p = c.Parameters.Add("@Tz", NpgsqlDbType.Text);
|
|
||||||
if (inserttz != null) p.Value = inserttz;
|
|
||||||
else p.Value = DBNull.Value;
|
|
||||||
c.Prepare();
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
await t.CommitAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We didn't crash! Get the new values stored locally.
|
|
||||||
_month = month;
|
|
||||||
_day = day;
|
|
||||||
_tz = inserttz;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes information of this user from the backing database.
|
|
||||||
/// The corresponding object reference should ideally be discarded after calling this.
|
|
||||||
/// </summary>
|
|
||||||
public async Task DeleteAsync(Database dbconfig)
|
|
||||||
{
|
|
||||||
using (var db = await dbconfig.OpenConnectionAsync())
|
|
||||||
{
|
|
||||||
await DoDeleteAsync(db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared between UpdateAsync and DeleteAsync
|
|
||||||
private async Task DoDeleteAsync(NpgsqlConnection dbconn)
|
|
||||||
{
|
|
||||||
using (var c = dbconn.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"delete from {BackingTable} "
|
|
||||||
+ "where guild_id = @Gid and user_id = @Uid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
|
|
||||||
c.Prepare();
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Database
|
|
||||||
public const string BackingTable = "user_birthdays";
|
|
||||||
|
|
||||||
internal static void SetUpDatabaseTable(NpgsqlConnection db)
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"create table if not exists {BackingTable} ("
|
|
||||||
+ $"guild_id bigint not null references {GuildStateInformation.BackingTable} ON DELETE CASCADE, "
|
|
||||||
+ "user_id bigint not null, "
|
|
||||||
+ "birth_month integer not null, "
|
|
||||||
+ "birth_day integer not null, "
|
|
||||||
+ "time_zone text null, "
|
|
||||||
+ "last_seen timestamptz not null default NOW(), "
|
|
||||||
+ "PRIMARY KEY (guild_id, user_id)"
|
|
||||||
+ ")";
|
|
||||||
c.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all known birthday records from the specified guild. No further filtering is done here.
|
|
||||||
/// </summary>
|
|
||||||
internal static IEnumerable<GuildUserSettings> GetGuildUsersAsync(Database dbsettings, ulong guildId)
|
|
||||||
{
|
|
||||||
using (var db = dbsettings.OpenConnectionAsync().GetAwaiter().GetResult())
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
// Take note of ordinals for use in the constructor
|
|
||||||
c.CommandText = "select guild_id, user_id, birth_month, birth_day, time_zone "
|
|
||||||
+ $"from {BackingTable} where guild_id = @Gid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
|
||||||
c.Prepare();
|
|
||||||
using (var r = c.ExecuteReader())
|
|
||||||
{
|
|
||||||
var result = new List<GuildUserSettings>();
|
|
||||||
while (r.Read())
|
|
||||||
{
|
|
||||||
result.Add(new GuildUserSettings(r));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,7 +22,7 @@ namespace BirthdayBot
|
||||||
var dc = new DiscordSocketConfig()
|
var dc = new DiscordSocketConfig()
|
||||||
{
|
{
|
||||||
AlwaysDownloadUsers = true,
|
AlwaysDownloadUsers = true,
|
||||||
DefaultRetryMode = Discord.RetryMode.RetryRatelimit,
|
DefaultRetryMode = RetryMode.RetryRatelimit,
|
||||||
MessageCacheSize = 0,
|
MessageCacheSize = 0,
|
||||||
TotalShards = cfg.ShardCount,
|
TotalShards = cfg.ShardCount,
|
||||||
ExclusiveBulkDelete = true
|
ExclusiveBulkDelete = true
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Discord.WebSocket;
|
using BirthdayBot.Data;
|
||||||
|
using Discord.WebSocket;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -22,7 +23,7 @@ namespace BirthdayBot.UserInterface
|
||||||
public const string NoParameterError = ":x: This command does not accept any parameters.";
|
public const string NoParameterError = ":x: This command does not accept any parameters.";
|
||||||
public const string InternalError = ":x: An internal bot error occurred. The bot maintainer has been notified of the issue.";
|
public const string InternalError = ":x: An internal bot error occurred. The bot maintainer has been notified of the issue.";
|
||||||
|
|
||||||
public delegate Task CommandHandler(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser);
|
public delegate Task CommandHandler(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser);
|
||||||
|
|
||||||
protected static Dictionary<string, string> TzNameMap {
|
protected static Dictionary<string, string> TzNameMap {
|
||||||
get {
|
get {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Discord;
|
using BirthdayBot.Data;
|
||||||
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -89,13 +90,13 @@ namespace BirthdayBot.UserInterface
|
||||||
return (helpRegular.Build(), helpConfig.Build());
|
return (helpRegular.Build(), helpConfig.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdHelp(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdHelp(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
=> await reqChannel.SendMessageAsync(embed: _helpEmbed);
|
=> await reqChannel.SendMessageAsync(embed: _helpEmbed);
|
||||||
|
|
||||||
private async Task CmdHelpConfig(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdHelpConfig(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
=> await reqChannel.SendMessageAsync(embed: _helpConfigEmbed);
|
=> await reqChannel.SendMessageAsync(embed: _helpConfigEmbed);
|
||||||
|
|
||||||
private async Task CmdHelpTzdata(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdHelpTzdata(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
const string tzhelp = "You may specify a time zone in order to have your birthday recognized with respect to your local time. "
|
const string tzhelp = "You may specify a time zone in order to have your birthday recognized with respect to your local time. "
|
||||||
+ "This bot only accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database).\n\n"
|
+ "This bot only accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database).\n\n"
|
||||||
|
@ -110,7 +111,7 @@ namespace BirthdayBot.UserInterface
|
||||||
await reqChannel.SendMessageAsync(embed: embed.Build());
|
await reqChannel.SendMessageAsync(embed: embed.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdHelpMessage(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdHelpMessage(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
const string msghelp = "The `message` and `messagepl` subcommands allow for editing the message sent into the announcement "
|
const string msghelp = "The `message` and `messagepl` subcommands allow for editing the message sent into the announcement "
|
||||||
+ "channel (defined with `{0}config channel`). This feature is separated across two commands:\n"
|
+ "channel (defined with `{0}config channel`). This feature is separated across two commands:\n"
|
||||||
|
@ -136,7 +137,7 @@ namespace BirthdayBot.UserInterface
|
||||||
await reqChannel.SendMessageAsync(embed: embed.Build());
|
await reqChannel.SendMessageAsync(embed: embed.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdInfo(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdInfo(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
var strStats = new StringBuilder();
|
var strStats = new StringBuilder();
|
||||||
var asmnm = System.Reflection.Assembly.GetExecutingAssembly().GetName();
|
var asmnm = System.Reflection.Assembly.GetExecutingAssembly().GetName();
|
||||||
|
|
|
@ -35,7 +35,7 @@ namespace BirthdayBot.UserInterface
|
||||||
new CommandDocumentation(new string[] { "when" }, "Displays the given user's birthday information.", null);
|
new CommandDocumentation(new string[] { "when" }, "Displays the given user's birthday information.", null);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task CmdWhen(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdWhen(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
// Requires a parameter
|
// Requires a parameter
|
||||||
if (param.Length == 1)
|
if (param.Length == 1)
|
||||||
|
@ -53,8 +53,7 @@ namespace BirthdayBot.UserInterface
|
||||||
|
|
||||||
SocketGuildUser searchTarget = null;
|
SocketGuildUser searchTarget = null;
|
||||||
|
|
||||||
ulong searchId = 0;
|
if (!TryGetUserId(search, out ulong searchId)) // ID lookup
|
||||||
if (!TryGetUserId(search, out searchId)) // ID lookup
|
|
||||||
{
|
{
|
||||||
// name lookup without discriminator
|
// name lookup without discriminator
|
||||||
foreach (var searchuser in reqChannel.Guild.Users)
|
foreach (var searchuser in reqChannel.Guild.Users)
|
||||||
|
@ -76,9 +75,8 @@ namespace BirthdayBot.UserInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var users = Instance.GuildCache[reqChannel.Guild.Id].Users;
|
var searchTargetData = await GuildUserConfiguration.LoadAsync(reqChannel.Guild.Id, searchId);
|
||||||
var searchTargetData = users.FirstOrDefault(u => u.UserId == searchTarget.Id);
|
if (!searchTargetData.IsKnown)
|
||||||
if (searchTargetData == null)
|
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync("I do not have birthday information for that user.");
|
await reqChannel.SendMessageAsync("I do not have birthday information for that user.");
|
||||||
return;
|
return;
|
||||||
|
@ -93,10 +91,10 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a file with all birthdays.
|
// Creates a file with all birthdays.
|
||||||
private async Task CmdList(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdList(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
// For now, we're restricting this command to moderators only. This may turn into an option later.
|
// For now, we're restricting this command to moderators only. This may turn into an option later.
|
||||||
if (!Instance.GuildCache[reqChannel.Guild.Id].IsUserModerator(reqUser))
|
if (!gconf.IsBotModerator(reqUser))
|
||||||
{
|
{
|
||||||
// Do not add detailed usage information to this error message.
|
// Do not add detailed usage information to this error message.
|
||||||
await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.");
|
await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.");
|
||||||
|
@ -120,7 +118,7 @@ namespace BirthdayBot.UserInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bdlist = await LoadList(reqChannel.Guild, false);
|
var bdlist = await GetSortedUsersAsync(reqChannel.Guild);
|
||||||
|
|
||||||
var filepath = Path.GetTempPath() + "birthdaybot-" + reqChannel.Guild.Id;
|
var filepath = Path.GetTempPath() + "birthdaybot-" + reqChannel.Guild.Id;
|
||||||
string fileoutput;
|
string fileoutput;
|
||||||
|
@ -158,13 +156,13 @@ namespace BirthdayBot.UserInterface
|
||||||
|
|
||||||
// "Recent and upcoming birthdays"
|
// "Recent and upcoming birthdays"
|
||||||
// The 'recent' bit removes time zone ambiguity and spares us from extra time zone processing here
|
// The 'recent' bit removes time zone ambiguity and spares us from extra time zone processing here
|
||||||
private async Task CmdUpcoming(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdUpcoming(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
var search = DateIndex(now.Month, now.Day) - 8; // begin search 8 days prior to current date UTC
|
var search = DateIndex(now.Month, now.Day) - 8; // begin search 8 days prior to current date UTC
|
||||||
if (search <= 0) search = 366 - Math.Abs(search);
|
if (search <= 0) search = 366 - Math.Abs(search);
|
||||||
|
|
||||||
var query = await LoadList(reqChannel.Guild, true);
|
var query = await GetSortedUsersAsync(reqChannel.Guild);
|
||||||
|
|
||||||
var output = new StringBuilder();
|
var output = new StringBuilder();
|
||||||
var resultCount = 0;
|
var resultCount = 0;
|
||||||
|
@ -219,20 +217,15 @@ namespace BirthdayBot.UserInterface
|
||||||
/// Fetches all guild birthdays and places them into an easily usable structure.
|
/// Fetches all guild birthdays and places them into an easily usable structure.
|
||||||
/// Users currently not in the guild are not included in the result.
|
/// Users currently not in the guild are not included in the result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<ListItem>> LoadList(SocketGuild guild, bool escapeFormat)
|
private async Task<List<ListItem>> GetSortedUsersAsync(SocketGuild guild)
|
||||||
{
|
{
|
||||||
var ping = Instance.GuildCache[guild.Id].AnnouncePing;
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
using (var db = await BotConfig.DatabaseSettings.OpenConnectionAsync())
|
c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserConfiguration.BackingTable
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserSettings.BackingTable
|
|
||||||
+ " where guild_id = @Gid order by birth_month, birth_day";
|
+ " where guild_id = @Gid order by birth_month, birth_day";
|
||||||
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
||||||
c.Prepare();
|
c.Prepare();
|
||||||
using (var r = await c.ExecuteReaderAsync())
|
using var r = await c.ExecuteReaderAsync();
|
||||||
{
|
|
||||||
var result = new List<ListItem>();
|
var result = new List<ListItem>();
|
||||||
while (await r.ReadAsync())
|
while (await r.ReadAsync())
|
||||||
{
|
{
|
||||||
|
@ -241,7 +234,7 @@ namespace BirthdayBot.UserInterface
|
||||||
var day = r.GetInt32(2);
|
var day = r.GetInt32(2);
|
||||||
|
|
||||||
var guildUser = guild.GetUser(id);
|
var guildUser = guild.GetUser(id);
|
||||||
if (guildUser == null) continue; // Skip users not in guild
|
if (guildUser == null) continue; // Skip user not in guild
|
||||||
|
|
||||||
result.Add(new ListItem()
|
result.Add(new ListItem()
|
||||||
{
|
{
|
||||||
|
@ -254,9 +247,6 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ListExportNormal(SocketGuildChannel channel, IEnumerable<ListItem> list)
|
private string ListExportNormal(SocketGuildChannel channel, IEnumerable<ListItem> list)
|
||||||
{
|
{
|
||||||
|
@ -295,7 +285,7 @@ namespace BirthdayBot.UserInterface
|
||||||
result.Append(',');
|
result.Append(',');
|
||||||
if (user.Nickname != null) result.Append(user.Nickname);
|
if (user.Nickname != null) result.Append(user.Nickname);
|
||||||
result.Append(',');
|
result.Append(',');
|
||||||
result.Append($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay.ToString("00")}");
|
result.Append($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}");
|
||||||
result.Append(',');
|
result.Append(',');
|
||||||
result.Append(item.BirthMonth);
|
result.Append(item.BirthMonth);
|
||||||
result.Append(',');
|
result.Append(',');
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Discord.WebSocket;
|
using BirthdayBot.Data;
|
||||||
|
using Discord.WebSocket;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -11,7 +12,7 @@ namespace BirthdayBot.UserInterface
|
||||||
{
|
{
|
||||||
private static readonly string ConfErrorPostfix =
|
private static readonly string ConfErrorPostfix =
|
||||||
$" Refer to the `{CommandPrefix}help-config` command for information on this command's usage.";
|
$" Refer to the `{CommandPrefix}help-config` command for information on this command's usage.";
|
||||||
private delegate Task ConfigSubcommand(string[] param, SocketTextChannel reqChannel);
|
private delegate Task ConfigSubcommand(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel);
|
||||||
|
|
||||||
private readonly Dictionary<string, ConfigSubcommand> _subcommands;
|
private readonly Dictionary<string, ConfigSubcommand> _subcommands;
|
||||||
private readonly Dictionary<string, CommandHandler> _usercommands;
|
private readonly Dictionary<string, CommandHandler> _usercommands;
|
||||||
|
@ -57,11 +58,10 @@ namespace BirthdayBot.UserInterface
|
||||||
"Perform certain commands on behalf of another user.", null);
|
"Perform certain commands on behalf of another user.", null);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task CmdConfigDispatch(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdConfigDispatch(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
// Ignore those without the proper permissions.
|
// Ignore those without the proper permissions.
|
||||||
// Requires either the manage guild permission or to be in the moderators role
|
if (!gconf.IsBotModerator(reqUser))
|
||||||
if (!Instance.GuildCache[reqUser.Guild.Id].IsUserModerator(reqUser))
|
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":x: This command may only be used by bot moderators.");
|
await reqChannel.SendMessageAsync(":x: This command may only be used by bot moderators.");
|
||||||
return;
|
return;
|
||||||
|
@ -73,7 +73,7 @@ namespace BirthdayBot.UserInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: Restrict 'modrole' to only guild managers
|
// Special case: Restrict 'modrole' to only guild managers, not mods
|
||||||
if (string.Equals(param[1], "modrole", StringComparison.OrdinalIgnoreCase) && !reqUser.GuildPermissions.ManageGuild)
|
if (string.Equals(param[1], "modrole", StringComparison.OrdinalIgnoreCase) && !reqUser.GuildPermissions.ManageGuild)
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":x: This command may only be used by those with the `Manage Server` permission.");
|
await reqChannel.SendMessageAsync(":x: This command may only be used by those with the `Manage Server` permission.");
|
||||||
|
@ -86,13 +86,13 @@ namespace BirthdayBot.UserInterface
|
||||||
|
|
||||||
if (_subcommands.TryGetValue(confparam[0], out ConfigSubcommand h))
|
if (_subcommands.TryGetValue(confparam[0], out ConfigSubcommand h))
|
||||||
{
|
{
|
||||||
await h(confparam, reqChannel);
|
await h(confparam, gconf, reqChannel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Configuration sub-commands
|
#region Configuration sub-commands
|
||||||
// Birthday role set
|
// Birthday role set
|
||||||
private async Task ScmdRole(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdRole(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
if (param.Length != 2)
|
if (param.Length != 2)
|
||||||
{
|
{
|
||||||
|
@ -112,13 +112,14 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Instance.GuildCache[guild.Id].UpdateRole(role.Id);
|
gconf.RoleId = role.Id;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
await reqChannel.SendMessageAsync($":white_check_mark: The birthday role has been set as **{role.Name}**.");
|
await reqChannel.SendMessageAsync($":white_check_mark: The birthday role has been set as **{role.Name}**.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping setting
|
// Ping setting
|
||||||
private async Task ScmdPing(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdPing(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
const string InputErr = ":x: You must specify either `off` or `on` in this setting.";
|
const string InputErr = ":x: You must specify either `off` or `on` in this setting.";
|
||||||
if (param.Length != 2)
|
if (param.Length != 2)
|
||||||
|
@ -146,26 +147,25 @@ namespace BirthdayBot.UserInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance.GuildCache[reqChannel.Guild.Id].UpdateAnnouncePing(setting);
|
gconf.AnnouncePing = setting;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
await reqChannel.SendMessageAsync(result);
|
await reqChannel.SendMessageAsync(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Announcement channel set
|
// Announcement channel set
|
||||||
private async Task ScmdChannel(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdChannel(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
if (param.Length == 1)
|
if (param.Length == 1) // No extra parameter. Unset announcement channel.
|
||||||
{
|
{
|
||||||
// No extra parameter. Unset announcement channel.
|
|
||||||
var gi = Instance.GuildCache[reqChannel.Guild.Id];
|
|
||||||
|
|
||||||
// Extra detail: Show a unique message if a channel hadn't been set prior.
|
// Extra detail: Show a unique message if a channel hadn't been set prior.
|
||||||
if (!gi.AnnounceChannelId.HasValue)
|
if (!gconf.AnnounceChannelId.HasValue)
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":x: There is no announcement channel set. Nothing to unset.");
|
await reqChannel.SendMessageAsync(":x: There is no announcement channel set. Nothing to unset.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
gi.UpdateAnnounceChannel(null);
|
gconf.AnnounceChannelId = null;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
await reqChannel.SendMessageAsync(":white_check_mark: The announcement channel has been unset.");
|
await reqChannel.SendMessageAsync(":white_check_mark: The announcement channel has been unset.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -204,7 +204,8 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the value
|
// Update the value
|
||||||
Instance.GuildCache[reqChannel.Guild.Id].UpdateAnnounceChannel(chId);
|
gconf.AnnounceChannelId = chId;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
|
|
||||||
// Report the success
|
// Report the success
|
||||||
await reqChannel.SendMessageAsync($":white_check_mark: The announcement channel is now set to <#{chId}>.");
|
await reqChannel.SendMessageAsync($":white_check_mark: The announcement channel is now set to <#{chId}>.");
|
||||||
|
@ -212,7 +213,7 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moderator role set
|
// Moderator role set
|
||||||
private async Task ScmdModRole(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdModRole(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
if (param.Length != 2)
|
if (param.Length != 2)
|
||||||
{
|
{
|
||||||
|
@ -228,27 +229,26 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Instance.GuildCache[guild.Id].UpdateModeratorRole(role.Id);
|
gconf.ModeratorRole = role.Id;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
await reqChannel.SendMessageAsync($":white_check_mark: The moderator role is now **{role.Name}**.");
|
await reqChannel.SendMessageAsync($":white_check_mark: The moderator role is now **{role.Name}**.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guild default time zone set/unset
|
// Guild default time zone set/unset
|
||||||
private async Task ScmdZone(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdZone(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
if (param.Length == 1)
|
if (param.Length == 1) // No extra parameter. Unset guild default time zone.
|
||||||
{
|
{
|
||||||
// No extra parameter. Unset guild default time zone.
|
|
||||||
var gi = Instance.GuildCache[reqChannel.Guild.Id];
|
|
||||||
|
|
||||||
// Extra detail: Show a unique message if there is no set zone.
|
// Extra detail: Show a unique message if there is no set zone.
|
||||||
if (!gi.AnnounceChannelId.HasValue)
|
if (!gconf.AnnounceChannelId.HasValue)
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":x: A default zone is not set. Nothing to unset.");
|
await reqChannel.SendMessageAsync(":x: A default zone is not set. Nothing to unset.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
gi.UpdateTimeZone(null);
|
gconf.TimeZone = null;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
await reqChannel.SendMessageAsync(":white_check_mark: The default time zone preference has been removed.");
|
await reqChannel.SendMessageAsync(":white_check_mark: The default time zone preference has been removed.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -266,7 +266,8 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update value
|
// Update value
|
||||||
Instance.GuildCache[reqChannel.Guild.Id].UpdateTimeZone(zone);
|
gconf.TimeZone = zone;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
|
|
||||||
// Report the success
|
// Report the success
|
||||||
await reqChannel.SendMessageAsync($":white_check_mark: The server's time zone has been set to **{zone}**.");
|
await reqChannel.SendMessageAsync($":white_check_mark: The server's time zone has been set to **{zone}**.");
|
||||||
|
@ -274,7 +275,7 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block/unblock individual non-manager users from using commands.
|
// Block/unblock individual non-manager users from using commands.
|
||||||
private async Task ScmdBlock(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdBlock(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
if (param.Length != 2)
|
if (param.Length != 2)
|
||||||
{
|
{
|
||||||
|
@ -284,43 +285,41 @@ namespace BirthdayBot.UserInterface
|
||||||
|
|
||||||
bool doBan = param[0].ToLower() == "block"; // true = block, false = unblock
|
bool doBan = param[0].ToLower() == "block"; // true = block, false = unblock
|
||||||
|
|
||||||
ulong inputId;
|
if (!TryGetUserId(param[1], out ulong inputId))
|
||||||
if (!TryGetUserId(param[1], out inputId))
|
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(BadUserError);
|
await reqChannel.SendMessageAsync(BadUserError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var gi = Instance.GuildCache[reqChannel.Guild.Id];
|
var isBanned = await gconf.IsUserBlockedAsync(inputId);
|
||||||
var isBanned = await gi.IsUserBlockedAsync(inputId);
|
|
||||||
if (doBan)
|
if (doBan)
|
||||||
{
|
{
|
||||||
if (!isBanned)
|
if (!isBanned)
|
||||||
{
|
{
|
||||||
await gi.BlockUserAsync(inputId);
|
await gconf.BlockUserAsync(inputId);
|
||||||
await reqChannel.SendMessageAsync(":white_check_mark: User has been blocked.");
|
await reqChannel.SendMessageAsync(":white_check_mark: User has been blocked.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// TODO bug: this is incorrectly always displayed when in moderated mode
|
||||||
await reqChannel.SendMessageAsync(":white_check_mark: User is already blocked.");
|
await reqChannel.SendMessageAsync(":white_check_mark: User is already blocked.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (isBanned)
|
if (await gconf.UnblockUserAsync(inputId))
|
||||||
{
|
{
|
||||||
await gi.UnbanUserAsync(inputId);
|
|
||||||
await reqChannel.SendMessageAsync(":white_check_mark: User is now unblocked.");
|
await reqChannel.SendMessageAsync(":white_check_mark: User is now unblocked.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":white_check_mark: The specified user has not been blocked.");
|
await reqChannel.SendMessageAsync(":white_check_mark: The specified user is not blocked.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "moderated on/off" - Sets/unsets moderated mode.
|
// "moderated on/off" - Sets/unsets moderated mode.
|
||||||
private async Task ScmdModerated(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdModerated(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
if (param.Length != 2)
|
if (param.Length != 2)
|
||||||
{
|
{
|
||||||
|
@ -334,26 +333,24 @@ namespace BirthdayBot.UserInterface
|
||||||
else if (parameter == "off") modSet = false;
|
else if (parameter == "off") modSet = false;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":x: Expected `on` or `off` as a parameter." + ConfErrorPostfix);
|
await reqChannel.SendMessageAsync(":x: Expecting `on` or `off` as a parameter." + ConfErrorPostfix);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var gi = Instance.GuildCache[reqChannel.Guild.Id];
|
if (gconf.IsModerated == modSet)
|
||||||
var currentSet = gi.IsModerated;
|
|
||||||
gi.UpdateModeratedMode(modSet);
|
|
||||||
|
|
||||||
if (currentSet == modSet)
|
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode is already {parameter}.");
|
await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode is already {parameter}.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
gconf.IsModerated = modSet;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode has been turned {parameter}.");
|
await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode has been turned {parameter}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets/unsets custom announcement message.
|
// Sets/unsets custom announcement message.
|
||||||
private async Task ScmdAnnounceMsg(string[] param, SocketTextChannel reqChannel)
|
private async Task ScmdAnnounceMsg(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
|
||||||
{
|
{
|
||||||
var plural = param[0].ToLower().EndsWith("pl");
|
var plural = param[0].ToLower().EndsWith("pl");
|
||||||
|
|
||||||
|
@ -370,17 +367,21 @@ namespace BirthdayBot.UserInterface
|
||||||
clear = true;
|
clear = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance.GuildCache[reqChannel.Guild.Id].UpdateAnnounceMessage(newmsg, plural);
|
(string, string) update;
|
||||||
const string report = ":white_check_mark: The {0} birthday announcement message has been {1}.";
|
if (!plural) update = (newmsg, gconf.AnnounceMessages.Item2);
|
||||||
await reqChannel.SendMessageAsync(string.Format(report, plural ? "plural" : "singular", clear ? "reset" : "updated"));
|
else update = (gconf.AnnounceMessages.Item1, newmsg);
|
||||||
|
gconf.AnnounceMessages = update;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
|
await reqChannel.SendMessageAsync(string.Format(":white_check_mark: The {0} birthday announcement message has been {1}.",
|
||||||
|
plural ? "plural" : "singular", clear ? "reset" : "updated"));
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// Execute command as another user
|
// Execute command as another user
|
||||||
private async Task CmdOverride(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdOverride(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
// Moderators only. As with config, silently drop if this check fails.
|
// Moderators only. As with config, silently drop if this check fails.
|
||||||
if (!Instance.GuildCache[reqUser.Guild.Id].IsUserModerator(reqUser)) return;
|
if (!gconf.IsBotModerator(reqUser)) return;
|
||||||
|
|
||||||
if (param.Length != 3)
|
if (param.Length != 3)
|
||||||
{
|
{
|
||||||
|
@ -389,8 +390,7 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second parameter: determine the user to act as
|
// Second parameter: determine the user to act as
|
||||||
ulong user = 0;
|
if (!TryGetUserId(param[1], out ulong user))
|
||||||
if (!TryGetUserId(param[1], out user))
|
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(BadUserError, embed: DocOverride.UsageEmbed);
|
await reqChannel.SendMessageAsync(BadUserError, embed: DocOverride.UsageEmbed);
|
||||||
return;
|
return;
|
||||||
|
@ -416,8 +416,7 @@ namespace BirthdayBot.UserInterface
|
||||||
// Add command prefix to input, just in case.
|
// Add command prefix to input, just in case.
|
||||||
overparam[0] = CommandPrefix + overparam[0].ToLower();
|
overparam[0] = CommandPrefix + overparam[0].ToLower();
|
||||||
}
|
}
|
||||||
CommandHandler action = null;
|
if (!_usercommands.TryGetValue(cmdsearch, out CommandHandler action))
|
||||||
if (!_usercommands.TryGetValue(cmdsearch, out action))
|
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync($":x: `{cmdsearch}` is not an overridable command.", embed: DocOverride.UsageEmbed);
|
await reqChannel.SendMessageAsync($":x: `{cmdsearch}` is not an overridable command.", embed: DocOverride.UsageEmbed);
|
||||||
return;
|
return;
|
||||||
|
@ -425,15 +424,14 @@ namespace BirthdayBot.UserInterface
|
||||||
|
|
||||||
// Preparations complete. Run the command.
|
// Preparations complete. Run the command.
|
||||||
await reqChannel.SendMessageAsync($"Executing `{cmdsearch.ToLower()}` on behalf of {overuser.Nickname ?? overuser.Username}:");
|
await reqChannel.SendMessageAsync($"Executing `{cmdsearch.ToLower()}` on behalf of {overuser.Nickname ?? overuser.Username}:");
|
||||||
await action.Invoke(overparam, reqChannel, overuser);
|
await action.Invoke(overparam, gconf, reqChannel, overuser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publicly available command that immediately processes the current guild,
|
// Publicly available command that immediately processes the current guild,
|
||||||
private async Task CmdTest(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdTest(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
// Moderators only. As with config, silently drop if this check fails.
|
// Moderators only. As with config, silently drop if this check fails.
|
||||||
if (!Instance.GuildCache[reqUser.Guild.Id].IsUserModerator(reqUser)) return;
|
if (!gconf.IsBotModerator(reqUser)) return;
|
||||||
// TODO fix this or incorporate into final output - checking existence in guild cache is a step in the process
|
|
||||||
|
|
||||||
if (param.Length != 1)
|
if (param.Length != 1)
|
||||||
{
|
{
|
||||||
|
@ -472,8 +470,7 @@ namespace BirthdayBot.UserInterface
|
||||||
if (rmatch.Success) input = rmatch.Groups["snowflake"].Value;
|
if (rmatch.Success) input = rmatch.Groups["snowflake"].Value;
|
||||||
|
|
||||||
// Attempt to get role by ID, or null
|
// Attempt to get role by ID, or null
|
||||||
ulong rid;
|
if (ulong.TryParse(input, out ulong rid))
|
||||||
if (ulong.TryParse(input, out rid))
|
|
||||||
{
|
{
|
||||||
return guild.GetRole(rid);
|
return guild.GetRole(rid);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using Discord.WebSocket;
|
using BirthdayBot.Data;
|
||||||
|
using Discord.WebSocket;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ namespace BirthdayBot.UserInterface
|
||||||
new CommandDocumentation(new string[] { "remove" }, "Removes your birthday information from this bot.", null);
|
new CommandDocumentation(new string[] { "remove" }, "Removes your birthday information from this bot.", null);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task CmdSet(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdSet(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
// Requires one parameter. Optionally two.
|
// Requires one parameter. Optionally two.
|
||||||
if (param.Length < 2 || param.Length > 3)
|
if (param.Length < 2 || param.Length > 3)
|
||||||
|
@ -140,9 +140,9 @@ namespace BirthdayBot.UserInterface
|
||||||
bool known; // Extra detail: Bot's response changes if the user was previously unknown.
|
bool known; // Extra detail: Bot's response changes if the user was previously unknown.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var user = Instance.GuildCache[reqChannel.Guild.Id].GetUser(reqUser.Id);
|
var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id);
|
||||||
known = user.IsKnown;
|
known = user.IsKnown;
|
||||||
await user.UpdateAsync(bmonth, bday, btz, BotConfig.DatabaseSettings);
|
await user.UpdateAsync(bmonth, bday, btz);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -161,7 +161,7 @@ namespace BirthdayBot.UserInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdZone(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdZone(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
if (param.Length != 2)
|
if (param.Length != 2)
|
||||||
{
|
{
|
||||||
|
@ -169,7 +169,7 @@ namespace BirthdayBot.UserInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = Instance.GuildCache[reqChannel.Guild.Id].GetUser(reqUser.Id);
|
var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id);
|
||||||
if (!user.IsKnown)
|
if (!user.IsKnown)
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":x: You may only update your time zone when you have a birthday registered."
|
await reqChannel.SendMessageAsync(":x: You may only update your time zone when you have a birthday registered."
|
||||||
|
@ -187,12 +187,12 @@ namespace BirthdayBot.UserInterface
|
||||||
reqChannel.SendMessageAsync(ex.Message, embed: DocZone.UsageEmbed).Wait();
|
reqChannel.SendMessageAsync(ex.Message, embed: DocZone.UsageEmbed).Wait();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await user.UpdateAsync(user.BirthMonth, user.BirthDay, btz, BotConfig.DatabaseSettings);
|
await user.UpdateAsync(user.BirthMonth, user.BirthDay, btz);
|
||||||
|
|
||||||
await reqChannel.SendMessageAsync($":white_check_mark: Your time zone has been updated to **{btz}**.");
|
await reqChannel.SendMessageAsync($":white_check_mark: Your time zone has been updated to **{btz}**.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdRemove(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
private async Task CmdRemove(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
||||||
{
|
{
|
||||||
// Parameter count check
|
// Parameter count check
|
||||||
if (param.Length != 1)
|
if (param.Length != 1)
|
||||||
|
@ -203,10 +203,9 @@ namespace BirthdayBot.UserInterface
|
||||||
|
|
||||||
// Extra detail: Send a notification if the user isn't actually known by the bot.
|
// Extra detail: Send a notification if the user isn't actually known by the bot.
|
||||||
bool known;
|
bool known;
|
||||||
var g = Instance.GuildCache[reqChannel.Guild.Id];
|
var u = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id);
|
||||||
known = g.GetUser(reqUser.Id).IsKnown;
|
known = u.IsKnown;
|
||||||
// Delete database and cache entry
|
await u.DeleteAsync();
|
||||||
await g.DeleteUserAsync(reqUser.Id);
|
|
||||||
if (!known)
|
if (!known)
|
||||||
{
|
{
|
||||||
await reqChannel.SendMessageAsync(":white_check_mark: This bot already does not contain your information.");
|
await reqChannel.SendMessageAsync(":white_check_mark: This bot already does not contain your information.");
|
||||||
|
|
Loading…
Reference in a new issue