mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-21 21:54:36 +00:00
Rework member cache downloading
Fill it quicker when immediately needed, removing the need for a request list and all relating to it in the corresponding background service. Additionally, updated usings, style, nullables in all affected files.
This commit is contained in:
parent
488ebfa163
commit
5e4d030467
8 changed files with 373 additions and 487 deletions
|
@ -1,12 +1,6 @@
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
using Discord.WebSocket;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
|
@ -46,7 +40,7 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task ProcessGuildAsync(SocketGuild guild) {
|
private static async Task ProcessGuildAsync(SocketGuild guild) {
|
||||||
// Load guild information - stop if local cache is unavailable.
|
// Load guild information - stop if local cache is unavailable.
|
||||||
if (!Common.HasMostMembersDownloaded(guild)) return;
|
if (!guild.HasAllMembers) return;
|
||||||
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
||||||
if (gc == null) return;
|
if (gc == null) return;
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,22 @@
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
using Discord;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rather than use up unnecessary resources by auto-downloading the user list in -every-
|
/// Proactively fills the user cache for guilds in which any birthday data already exists.
|
||||||
/// server we're in, this service checks if fetching the user list is warranted for each
|
|
||||||
/// guild before proceeding to request it.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class SelectiveAutoUserDownload : BackgroundService {
|
class SelectiveAutoUserDownload : BackgroundService {
|
||||||
private readonly HashSet<ulong> _fetchRequests = new();
|
|
||||||
|
|
||||||
public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { }
|
public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { }
|
||||||
|
|
||||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
IEnumerable<ulong> requests;
|
|
||||||
lock (_fetchRequests) {
|
|
||||||
requests = _fetchRequests.ToArray();
|
|
||||||
_fetchRequests.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||||
// Has the potential to disconnect while in the middle of processing.
|
// Has the potential to disconnect while in the middle of processing.
|
||||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) return;
|
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) return;
|
||||||
|
|
||||||
// Determine if there is action to be taken...
|
// Determine if there is action to be taken...
|
||||||
if (guild.HasAllMembers) continue;
|
if (!guild.HasAllMembers && await GuildUserAnyAsync(guild.Id).ConfigureAwait(false)) {
|
||||||
if (requests.Contains(guild.Id) || await GuildUserAnyAsync(guild.Id).ConfigureAwait(false)) {
|
await guild.DownloadUsersAsync().ConfigureAwait(false); // This is already on a separate thread; no need to Task.Run
|
||||||
await guild.DownloadUsersAsync().ConfigureAwait(false);
|
await Task.Delay(200, CancellationToken.None).ConfigureAwait(false); // Must delay, or else it seems to hang...
|
||||||
// Must delay after a download request. Seems to hang indefinitely otherwise.
|
|
||||||
await Task.Delay(300, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,8 +34,4 @@ class SelectiveAutoUserDownload : BackgroundService {
|
||||||
using var r = await c.ExecuteReaderAsync(CancellationToken.None).ConfigureAwait(false);
|
using var r = await c.ExecuteReaderAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
return r.Read();
|
return r.Read();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RequestDownload(ulong guildId) {
|
|
||||||
lock (_fetchRequests) _fetchRequests.Add(guildId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>3.2.2</Version>
|
<Version>3.2.3</Version>
|
||||||
<Authors>NoiTheCat</Authors>
|
<Authors>NoiTheCat</Authors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
21
Common.cs
21
Common.cs
|
@ -1,6 +1,4 @@
|
||||||
using Discord.WebSocket;
|
using System.Text;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace BirthdayBot;
|
namespace BirthdayBot;
|
||||||
|
|
||||||
|
@ -33,21 +31,4 @@ static class Common {
|
||||||
{ 1, "Jan" }, { 2, "Feb" }, { 3, "Mar" }, { 4, "Apr" }, { 5, "May" }, { 6, "Jun" },
|
{ 1, "Jan" }, { 2, "Feb" }, { 3, "Mar" }, { 4, "Apr" }, { 5, "May" }, { 6, "Jun" },
|
||||||
{ 7, "Jul" }, { 8, "Aug" }, { 9, "Sep" }, { 10, "Oct" }, { 11, "Nov" }, { 12, "Dec" }
|
{ 7, "Jul" }, { 8, "Aug" }, { 9, "Sep" }, { 10, "Oct" }, { 11, "Nov" }, { 12, "Dec" }
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An alternative to <see cref="SocketGuild.HasAllMembers"/>.
|
|
||||||
/// Returns true if *most* members have been downloaded.
|
|
||||||
/// </summary>
|
|
||||||
public static bool HasMostMembersDownloaded(SocketGuild guild) {
|
|
||||||
if (guild.HasAllMembers) return true;
|
|
||||||
if (guild.MemberCount > 30) {
|
|
||||||
// For guilds of size over 30, require 85% or more of the members to be known
|
|
||||||
// (26/30, 42/50, 255/300, etc)
|
|
||||||
int threshold = (int)(guild.MemberCount * 0.85);
|
|
||||||
return guild.DownloadedMemberCount >= threshold;
|
|
||||||
} else {
|
|
||||||
// For smaller guilds, fail if two or more members are missing
|
|
||||||
return guild.MemberCount - guild.DownloadedMemberCount <= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,8 +80,6 @@ class ShardInstance : IDisposable {
|
||||||
|
|
||||||
public void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message);
|
public void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message);
|
||||||
|
|
||||||
public void RequestDownloadUsers(ulong guildId) => _background.UserDownloader.RequestDownload(guildId);
|
|
||||||
|
|
||||||
#region Event handling
|
#region Event handling
|
||||||
private Task Client_Log(LogMessage arg) {
|
private Task Client_Log(LogMessage arg) {
|
||||||
// TODO revise this some time, filters might need to be modified by now
|
// TODO revise this some time, filters might need to be modified by now
|
||||||
|
|
|
@ -1,99 +1,94 @@
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
using Discord.WebSocket;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.UserInterface
|
namespace BirthdayBot.UserInterface;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common base class for common constants and variables.
|
/// Common base class for common constants and variables.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class CommandsCommon
|
internal abstract class CommandsCommon {
|
||||||
{
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
public const string CommandPrefix = "bt.";
|
public const string CommandPrefix = "bt.";
|
||||||
#else
|
#else
|
||||||
public const string CommandPrefix = "bb.";
|
public const string CommandPrefix = "bb.";
|
||||||
#endif
|
#endif
|
||||||
public const string BadUserError = ":x: Unable to find user. Specify their `@` mention or their ID.";
|
public const string BadUserError = ":x: Unable to find user. Specify their `@` mention or their ID.";
|
||||||
public const string ParameterError = ":x: Invalid usage. Refer to how to use the command and try again.";
|
public const string ParameterError = ":x: Invalid usage. Refer to how to use the command and try again.";
|
||||||
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 const string UsersNotDownloadedError = ":eight_spoked_asterisk: Still catching up... Please try the command again in a few minutes.";
|
public const string MemberCacheEmptyError = ":warning: Please try the command again.";
|
||||||
|
|
||||||
public delegate Task CommandHandler(ShardInstance instance, GuildConfiguration gconf,
|
public delegate Task CommandHandler(ShardInstance instance, GuildConfiguration gconf,
|
||||||
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser);
|
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser);
|
||||||
|
|
||||||
protected static Dictionary<string, string> TzNameMap {
|
protected static ReadOnlyDictionary<string, string> TzNameMap { get; }
|
||||||
get {
|
protected static Regex ChannelMention { get; } = new Regex(@"<#(\d+)>");
|
||||||
// Because IDateTimeZoneProvider.GetZoneOrNull is not case sensitive:
|
protected static Regex UserMention { get; } = new Regex(@"\!?(\d+)>");
|
||||||
// Getting every existing zone name and mapping it onto a dictionary. Now a case-insensitive
|
|
||||||
// search can be made with the accepted value retrieved as a result.
|
|
||||||
if (_tzNameMap == null)
|
|
||||||
{
|
|
||||||
_tzNameMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) _tzNameMap.Add(name, name);
|
|
||||||
}
|
|
||||||
return _tzNameMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
protected static Regex ChannelMention { get; } = new Regex(@"<#(\d+)>");
|
|
||||||
protected static Regex UserMention { get; } = new Regex(@"\!?(\d+)>");
|
|
||||||
private static Dictionary<string, string> _tzNameMap; // Value set by getter property on first read
|
|
||||||
|
|
||||||
protected Configuration BotConfig { get; }
|
protected Configuration BotConfig { get; }
|
||||||
|
|
||||||
protected CommandsCommon(Configuration db)
|
protected CommandsCommon(Configuration db) {
|
||||||
{
|
BotConfig = db;
|
||||||
BotConfig = db;
|
}
|
||||||
|
|
||||||
|
static CommandsCommon() {
|
||||||
|
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) dict.Add(name, name);
|
||||||
|
TzNameMap = new(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On command dispatcher initialization, it will retrieve all available commands through here.
|
||||||
|
/// </summary>
|
||||||
|
public abstract IEnumerable<(string, CommandHandler)> Commands { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks given time zone input. Returns a valid string for use with NodaTime.
|
||||||
|
/// </summary>
|
||||||
|
protected static string ParseTimeZone(string tzinput) {
|
||||||
|
if (tzinput.Equals("Asia/Calcutta", StringComparison.OrdinalIgnoreCase)) tzinput = "Asia/Kolkata";
|
||||||
|
if (!TzNameMap.TryGetValue(tzinput, out string? tz)) throw new FormatException(":x: Unexpected time zone name."
|
||||||
|
+ $" Refer to `{CommandPrefix}help-tzdata` to help determine the correct value.");
|
||||||
|
return tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given user input where a user-like parameter is expected, attempts to resolve to an ID value.
|
||||||
|
/// Input must be a mention or explicit ID. No name resolution is done here.
|
||||||
|
/// </summary>
|
||||||
|
protected static bool TryGetUserId(string input, out ulong result) {
|
||||||
|
string doParse;
|
||||||
|
var m = UserMention.Match(input);
|
||||||
|
if (m.Success) doParse = m.Groups[1].Value;
|
||||||
|
else doParse = input;
|
||||||
|
|
||||||
|
if (ulong.TryParse(doParse, out ulong resultVal)) {
|
||||||
|
result = resultVal;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
result = default;
|
||||||
/// On command dispatcher initialization, it will retrieve all available commands through here.
|
return false;
|
||||||
/// </summary>
|
}
|
||||||
public abstract IEnumerable<(string, CommandHandler)> Commands { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks given time zone input. Returns a valid string for use with NodaTime.
|
/// An alternative to <see cref="SocketGuild.HasAllMembers"/> to be called by command handlers needing a full member cache.
|
||||||
/// </summary>
|
/// Creates a download request if necessary.
|
||||||
protected static string ParseTimeZone(string tzinput)
|
/// </summary>
|
||||||
{
|
/// <returns>
|
||||||
string tz = null;
|
/// True if the member cache is already filled, false otherwise.
|
||||||
if (tzinput != null)
|
/// </returns>
|
||||||
{
|
/// <remarks>
|
||||||
if (tzinput.Equals("Asia/Calcutta", StringComparison.OrdinalIgnoreCase)) tzinput = "Asia/Kolkata";
|
/// Any updates to the member cache aren't accessible until the event handler finishes execution, meaning proactive downloading
|
||||||
|
/// is necessary, and is handled by <seealso cref="BackgroundServices.SelectiveAutoUserDownload"/>. In situations where
|
||||||
// Just check if the input exists in the map. Get the "true" value, or reject it altogether.
|
/// this approach fails, this is to be called, and the user must be asked to attempt the command again if this returns false.
|
||||||
if (!TzNameMap.TryGetValue(tzinput, out tz))
|
/// </remarks>
|
||||||
{
|
protected static async Task<bool> HasMemberCacheAsync(SocketGuild guild) {
|
||||||
throw new FormatException(":x: Unexpected time zone name."
|
if (guild.HasAllMembers) return true;
|
||||||
+ $" Refer to `{CommandPrefix}help-tzdata` to help determine the correct value.");
|
// Event handling thread hangs if awaited normally or used with Task.Run
|
||||||
}
|
await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
|
||||||
}
|
return false;
|
||||||
return tz;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Given user input where a user-like parameter is expected, attempts to resolve to an ID value.
|
|
||||||
/// Input must be a mention or explicit ID. No name resolution is done here.
|
|
||||||
/// </summary>
|
|
||||||
protected static bool TryGetUserId(string input, out ulong result)
|
|
||||||
{
|
|
||||||
string doParse;
|
|
||||||
var m = UserMention.Match(input);
|
|
||||||
if (m.Success) doParse = m.Groups[1].Value;
|
|
||||||
else doParse = input;
|
|
||||||
|
|
||||||
if (ulong.TryParse(doParse, out ulong resultVal)) {
|
|
||||||
result = resultVal;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,367 +1,313 @@
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
using Discord.WebSocket;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.UserInterface
|
namespace BirthdayBot.UserInterface;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Commands for listing upcoming and all birthdays.
|
|
||||||
/// </summary>
|
|
||||||
internal class ListingCommands : CommandsCommon
|
|
||||||
{
|
|
||||||
public ListingCommands(Configuration db) : base(db) { }
|
|
||||||
|
|
||||||
public override IEnumerable<(string, CommandHandler)> Commands
|
/// <summary>
|
||||||
=> new List<(string, CommandHandler)>()
|
/// Commands for listing upcoming and all birthdays.
|
||||||
{
|
/// </summary>
|
||||||
|
internal class ListingCommands : CommandsCommon {
|
||||||
|
public ListingCommands(Configuration db) : base(db) { }
|
||||||
|
|
||||||
|
public override IEnumerable<(string, CommandHandler)> Commands
|
||||||
|
=> new List<(string, CommandHandler)>()
|
||||||
|
{
|
||||||
("list", CmdList),
|
("list", CmdList),
|
||||||
("upcoming", CmdUpcoming),
|
("upcoming", CmdUpcoming),
|
||||||
("recent", CmdUpcoming),
|
("recent", CmdUpcoming),
|
||||||
("when", CmdWhen)
|
("when", CmdWhen)
|
||||||
};
|
};
|
||||||
|
|
||||||
#region Documentation
|
#region Documentation
|
||||||
public static readonly CommandDocumentation DocList =
|
public static readonly CommandDocumentation DocList =
|
||||||
new(new string[] { "list" }, "Exports all birthdays to a file."
|
new(new string[] { "list" }, "Exports all birthdays to a file."
|
||||||
+ " Accepts `csv` as an optional parameter.", null);
|
+ " Accepts `csv` as an optional parameter.", null);
|
||||||
public static readonly CommandDocumentation DocUpcoming =
|
public static readonly CommandDocumentation DocUpcoming =
|
||||||
new(new string[] { "recent", "upcoming" }, "Lists recent and upcoming birthdays.", null);
|
new(new string[] { "recent", "upcoming" }, "Lists recent and upcoming birthdays.", null);
|
||||||
public static readonly CommandDocumentation DocWhen =
|
public static readonly CommandDocumentation DocWhen =
|
||||||
new(new string[] { "when" }, "Displays the given user's birthday information.", null);
|
new(new string[] { "when" }, "Displays the given user's birthday information.", null);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task CmdWhen(ShardInstance instance, GuildConfiguration gconf,
|
private async Task CmdWhen(ShardInstance instance, GuildConfiguration gconf,
|
||||||
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
|
||||||
{
|
if (!await HasMemberCacheAsync(reqChannel.Guild)) {
|
||||||
if (!Common.HasMostMembersDownloaded(reqChannel.Guild))
|
await reqChannel.SendMessageAsync(MemberCacheEmptyError);
|
||||||
{
|
return;
|
||||||
instance.RequestDownloadUsers(reqChannel.Guild.Id);
|
|
||||||
await reqChannel.SendMessageAsync(UsersNotDownloadedError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Requires a parameter
|
|
||||||
if (param.Length == 1)
|
|
||||||
{
|
|
||||||
await reqChannel.SendMessageAsync(ParameterError, embed: DocWhen.UsageEmbed).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var search = param[1];
|
|
||||||
if (param.Length == 3)
|
|
||||||
{
|
|
||||||
// param maxes out at 3 values. param[2] might contain part of the search string (if name has a space)
|
|
||||||
search += " " + param[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
SocketGuildUser searchTarget = null;
|
|
||||||
|
|
||||||
if (!TryGetUserId(search, out ulong searchId)) // ID lookup
|
|
||||||
{
|
|
||||||
// name lookup without discriminator
|
|
||||||
foreach (var searchuser in reqChannel.Guild.Users)
|
|
||||||
{
|
|
||||||
if (string.Equals(search, searchuser.Username, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
searchTarget = searchuser;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
searchTarget = reqChannel.Guild.GetUser(searchId);
|
|
||||||
}
|
|
||||||
if (searchTarget == null)
|
|
||||||
{
|
|
||||||
await reqChannel.SendMessageAsync(BadUserError, embed: DocWhen.UsageEmbed).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchTargetData = await GuildUserConfiguration.LoadAsync(reqChannel.Guild.Id, searchId).ConfigureAwait(false);
|
|
||||||
if (!searchTargetData.IsKnown)
|
|
||||||
{
|
|
||||||
await reqChannel.SendMessageAsync("I do not have birthday information for that user.").ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string result = Common.FormatName(searchTarget, false);
|
|
||||||
result += ": ";
|
|
||||||
result += $"`{searchTargetData.BirthDay:00}-{Common.MonthNames[searchTargetData.BirthMonth]}`";
|
|
||||||
result += searchTargetData.TimeZone == null ? "" : $" - `{searchTargetData.TimeZone}`";
|
|
||||||
|
|
||||||
await reqChannel.SendMessageAsync(result).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a file with all birthdays.
|
// Requires a parameter
|
||||||
private async Task CmdList(ShardInstance instance, GuildConfiguration gconf,
|
if (param.Length == 1) {
|
||||||
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
await reqChannel.SendMessageAsync(ParameterError, embed: DocWhen.UsageEmbed).ConfigureAwait(false);
|
||||||
{
|
return;
|
||||||
// For now, we're restricting this command to moderators only. This may turn into an option later.
|
|
||||||
if (!gconf.IsBotModerator(reqUser))
|
|
||||||
{
|
|
||||||
// Do not add detailed usage information to this error message.
|
|
||||||
await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.").ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Common.HasMostMembersDownloaded(reqChannel.Guild))
|
|
||||||
{
|
|
||||||
instance.RequestDownloadUsers(reqChannel.Guild.Id);
|
|
||||||
await reqChannel.SendMessageAsync(UsersNotDownloadedError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool useCsv = false;
|
|
||||||
// Check for CSV option
|
|
||||||
if (param.Length == 2)
|
|
||||||
{
|
|
||||||
if (param[1].ToLower() == "csv") useCsv = true;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await reqChannel.SendMessageAsync(":x: That is not available as an export format.", embed: DocList.UsageEmbed)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (param.Length > 2)
|
|
||||||
{
|
|
||||||
await reqChannel.SendMessageAsync(ParameterError, embed: DocList.UsageEmbed).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bdlist = await GetSortedUsersAsync(reqChannel.Guild).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var filepath = Path.GetTempPath() + "birthdaybot-" + reqChannel.Guild.Id;
|
|
||||||
string fileoutput;
|
|
||||||
if (useCsv)
|
|
||||||
{
|
|
||||||
fileoutput = ListExportCsv(reqChannel, bdlist);
|
|
||||||
filepath += ".csv";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fileoutput = ListExportNormal(reqChannel, bdlist);
|
|
||||||
filepath += ".txt.";
|
|
||||||
}
|
|
||||||
await File.WriteAllTextAsync(filepath, fileoutput, Encoding.UTF8).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await reqChannel.SendFileAsync(filepath, $"Exported {bdlist.Count} birthdays to file.").ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Discord.Net.HttpException)
|
|
||||||
{
|
|
||||||
reqChannel.SendMessageAsync(":x: Unable to send list due to a permissions issue. Check the 'Attach Files' permission.").Wait();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Program.Log("Listing", ex.ToString());
|
|
||||||
reqChannel.SendMessageAsync(InternalError).Wait();
|
|
||||||
// TODO webhook report
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
File.Delete(filepath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Recent and upcoming birthdays"
|
var search = param[1];
|
||||||
// The 'recent' bit removes time zone ambiguity and spares us from extra time zone processing here
|
if (param.Length == 3) {
|
||||||
private async Task CmdUpcoming(ShardInstance instance, GuildConfiguration gconf,
|
// param maxes out at 3 values. param[2] might contain part of the search string (if name has a space)
|
||||||
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
search += " " + param[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
SocketGuildUser? searchTarget = null;
|
||||||
|
|
||||||
|
if (!TryGetUserId(search, out ulong searchId)) // ID lookup
|
||||||
{
|
{
|
||||||
if (!Common.HasMostMembersDownloaded(reqChannel.Guild))
|
// name lookup without discriminator
|
||||||
{
|
foreach (var searchuser in reqChannel.Guild.Users) {
|
||||||
instance.RequestDownloadUsers(reqChannel.Guild.Id);
|
if (string.Equals(search, searchuser.Username, StringComparison.OrdinalIgnoreCase)) {
|
||||||
await reqChannel.SendMessageAsync(UsersNotDownloadedError);
|
searchTarget = searchuser;
|
||||||
return;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
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);
|
|
||||||
|
|
||||||
var query = await GetSortedUsersAsync(reqChannel.Guild).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var output = new StringBuilder();
|
|
||||||
var resultCount = 0;
|
|
||||||
output.AppendLine("Recent and upcoming birthdays:");
|
|
||||||
for (int count = 0; count <= 21; count++) // cover 21 days total (7 prior, current day, 14 upcoming)
|
|
||||||
{
|
|
||||||
var results = from item in query
|
|
||||||
where item.DateIndex == search
|
|
||||||
select item;
|
|
||||||
|
|
||||||
// push up search by 1 now, in case we back out early
|
|
||||||
search += 1;
|
|
||||||
if (search > 366) search = 1; // wrap to beginning of year
|
|
||||||
|
|
||||||
if (!results.Any()) continue; // back out early
|
|
||||||
resultCount += results.Count();
|
|
||||||
|
|
||||||
// Build sorted name list
|
|
||||||
var names = new List<string>();
|
|
||||||
foreach (var item in results)
|
|
||||||
{
|
|
||||||
names.Add(item.DisplayName);
|
|
||||||
}
|
|
||||||
names.Sort(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var first = true;
|
|
||||||
output.AppendLine();
|
|
||||||
output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
|
|
||||||
foreach (var item in names)
|
|
||||||
{
|
|
||||||
// If the output is starting to fill up, send out this message and prepare a new one.
|
|
||||||
if (output.Length > 800)
|
|
||||||
{
|
|
||||||
await reqChannel.SendMessageAsync(output.ToString()).ConfigureAwait(false);
|
|
||||||
output.Clear();
|
|
||||||
first = true;
|
|
||||||
output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first) first = false;
|
|
||||||
else output.Append(", ");
|
|
||||||
output.Append(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
searchTarget = reqChannel.Guild.GetUser(searchId);
|
||||||
|
}
|
||||||
|
if (searchTarget == null) {
|
||||||
|
await reqChannel.SendMessageAsync(BadUserError, embed: DocWhen.UsageEmbed).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (resultCount == 0)
|
var searchTargetData = await GuildUserConfiguration.LoadAsync(reqChannel.Guild.Id, searchId).ConfigureAwait(false);
|
||||||
await reqChannel.SendMessageAsync(
|
if (!searchTargetData.IsKnown) {
|
||||||
"There are no recent or upcoming birthdays (within the last 7 days and/or next 21 days).")
|
await reqChannel.SendMessageAsync("I do not have birthday information for that user.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string result = Common.FormatName(searchTarget, false);
|
||||||
|
result += ": ";
|
||||||
|
result += $"`{searchTargetData.BirthDay:00}-{Common.MonthNames[searchTargetData.BirthMonth]}`";
|
||||||
|
result += searchTargetData.TimeZone == null ? "" : $" - `{searchTargetData.TimeZone}`";
|
||||||
|
|
||||||
|
await reqChannel.SendMessageAsync(result).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a file with all birthdays.
|
||||||
|
private async Task CmdList(ShardInstance instance, GuildConfiguration gconf,
|
||||||
|
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
|
||||||
|
// For now, we're restricting this command to moderators only. This may turn into an option later.
|
||||||
|
if (!gconf.IsBotModerator(reqUser)) {
|
||||||
|
// Do not add detailed usage information to this error message.
|
||||||
|
await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await HasMemberCacheAsync(reqChannel.Guild)) {
|
||||||
|
await reqChannel.SendMessageAsync(MemberCacheEmptyError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useCsv = false;
|
||||||
|
// Check for CSV option
|
||||||
|
if (param.Length == 2) {
|
||||||
|
if (param[1].ToLower() == "csv") useCsv = true;
|
||||||
|
else {
|
||||||
|
await reqChannel.SendMessageAsync(":x: That is not available as an export format.", embed: DocList.UsageEmbed)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
else
|
return;
|
||||||
await reqChannel.SendMessageAsync(output.ToString()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches all guild birthdays and places them into an easily usable structure.
|
|
||||||
/// Users currently not in the guild are not included in the result.
|
|
||||||
/// </summary>
|
|
||||||
private static async Task<List<ListItem>> GetSortedUsersAsync(SocketGuild guild)
|
|
||||||
{
|
|
||||||
using var db = await Database.OpenConnectionAsync();
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserConfiguration.BackingTable
|
|
||||||
+ " where guild_id = @Gid order by birth_month, birth_day";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
|
||||||
c.Prepare();
|
|
||||||
using var r = await c.ExecuteReaderAsync();
|
|
||||||
var result = new List<ListItem>();
|
|
||||||
while (await r.ReadAsync())
|
|
||||||
{
|
|
||||||
var id = (ulong)r.GetInt64(0);
|
|
||||||
var month = r.GetInt32(1);
|
|
||||||
var day = r.GetInt32(2);
|
|
||||||
|
|
||||||
var guildUser = guild.GetUser(id);
|
|
||||||
if (guildUser == null) continue; // Skip user not in guild
|
|
||||||
|
|
||||||
result.Add(new ListItem()
|
|
||||||
{
|
|
||||||
BirthMonth = month,
|
|
||||||
BirthDay = day,
|
|
||||||
DateIndex = DateIndex(month, day),
|
|
||||||
UserId = guildUser.Id,
|
|
||||||
DisplayName = Common.FormatName(guildUser, false)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return result;
|
} else if (param.Length > 2) {
|
||||||
|
await reqChannel.SendMessageAsync(ParameterError, embed: DocList.UsageEmbed).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ListExportNormal(SocketGuildChannel channel, IEnumerable<ListItem> list)
|
var bdlist = await GetSortedUsersAsync(reqChannel.Guild).ConfigureAwait(false);
|
||||||
{
|
|
||||||
// Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]"
|
var filepath = Path.GetTempPath() + "birthdaybot-" + reqChannel.Guild.Id;
|
||||||
var result = new StringBuilder();
|
string fileoutput;
|
||||||
result.AppendLine("Birthdays in " + channel.Guild.Name);
|
if (useCsv) {
|
||||||
result.AppendLine();
|
fileoutput = ListExportCsv(reqChannel, bdlist);
|
||||||
foreach (var item in list)
|
filepath += ".csv";
|
||||||
{
|
} else {
|
||||||
var user = channel.Guild.GetUser(item.UserId);
|
fileoutput = ListExportNormal(reqChannel, bdlist);
|
||||||
if (user == null) continue; // User disappeared in the instant between getting list and processing
|
filepath += ".txt.";
|
||||||
result.Append($"● {Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}: ");
|
|
||||||
result.Append(item.UserId);
|
|
||||||
result.Append(" " + user.Username + "#" + user.Discriminator);
|
|
||||||
if (user.Nickname != null) result.Append(" - Nickname: " + user.Nickname);
|
|
||||||
result.AppendLine();
|
|
||||||
}
|
|
||||||
return result.ToString();
|
|
||||||
}
|
}
|
||||||
|
await File.WriteAllTextAsync(filepath, fileoutput, Encoding.UTF8).ConfigureAwait(false);
|
||||||
|
|
||||||
private string ListExportCsv(SocketGuildChannel channel, IEnumerable<ListItem> list)
|
try {
|
||||||
{
|
await reqChannel.SendFileAsync(filepath, $"Exported {bdlist.Count} birthdays to file.").ConfigureAwait(false);
|
||||||
// Output: User ID, Username, Nickname, Month-Day, Month, Day
|
} catch (Discord.Net.HttpException) {
|
||||||
var result = new StringBuilder();
|
reqChannel.SendMessageAsync(":x: Unable to send list due to a permissions issue. Check the 'Attach Files' permission.").Wait();
|
||||||
|
} catch (Exception ex) {
|
||||||
// Conforming to RFC 4180; with header
|
Program.Log("Listing", ex.ToString());
|
||||||
result.Append("UserId,Username,Nickname,MonthDayDisp,Month,Day");
|
reqChannel.SendMessageAsync(InternalError).Wait();
|
||||||
result.Append("\r\n"); // crlf line break is specified by the standard
|
// TODO webhook report
|
||||||
foreach (var item in list)
|
} finally {
|
||||||
{
|
File.Delete(filepath);
|
||||||
var user = channel.Guild.GetUser(item.UserId);
|
|
||||||
if (user == null) continue; // User disappeared in the instant between getting list and processing
|
|
||||||
result.Append(item.UserId);
|
|
||||||
result.Append(',');
|
|
||||||
result.Append(CsvEscape(user.Username + "#" + user.Discriminator));
|
|
||||||
result.Append(',');
|
|
||||||
if (user.Nickname != null) result.Append(user.Nickname);
|
|
||||||
result.Append(',');
|
|
||||||
result.Append($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}");
|
|
||||||
result.Append(',');
|
|
||||||
result.Append(item.BirthMonth);
|
|
||||||
result.Append(',');
|
|
||||||
result.Append(item.BirthDay);
|
|
||||||
result.Append("\r\n");
|
|
||||||
}
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CsvEscape(string input)
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
result.Append('"');
|
|
||||||
foreach (var ch in input)
|
|
||||||
{
|
|
||||||
if (ch == '"') result.Append('"');
|
|
||||||
result.Append(ch);
|
|
||||||
}
|
|
||||||
result.Append('"');
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int DateIndex(int month, int day)
|
|
||||||
{
|
|
||||||
var dateindex = 0;
|
|
||||||
// Add month offsets
|
|
||||||
if (month > 1) dateindex += 31; // Offset January
|
|
||||||
if (month > 2) dateindex += 29; // Offset February (incl. leap day)
|
|
||||||
if (month > 3) dateindex += 31; // etc
|
|
||||||
if (month > 4) dateindex += 30;
|
|
||||||
if (month > 5) dateindex += 31;
|
|
||||||
if (month > 6) dateindex += 30;
|
|
||||||
if (month > 7) dateindex += 31;
|
|
||||||
if (month > 8) dateindex += 31;
|
|
||||||
if (month > 9) dateindex += 30;
|
|
||||||
if (month > 10) dateindex += 31;
|
|
||||||
if (month > 11) dateindex += 30;
|
|
||||||
dateindex += day;
|
|
||||||
return dateindex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ListItem
|
|
||||||
{
|
|
||||||
public int DateIndex;
|
|
||||||
public int BirthMonth;
|
|
||||||
public int BirthDay;
|
|
||||||
public ulong UserId;
|
|
||||||
public string DisplayName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Recent and upcoming birthdays"
|
||||||
|
// The 'recent' bit removes time zone ambiguity and spares us from extra time zone processing here
|
||||||
|
private async Task CmdUpcoming(ShardInstance instance, GuildConfiguration gconf,
|
||||||
|
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
|
||||||
|
if (!await HasMemberCacheAsync(reqChannel.Guild)) {
|
||||||
|
await reqChannel.SendMessageAsync(MemberCacheEmptyError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
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);
|
||||||
|
|
||||||
|
var query = await GetSortedUsersAsync(reqChannel.Guild).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var output = new StringBuilder();
|
||||||
|
var resultCount = 0;
|
||||||
|
output.AppendLine("Recent and upcoming birthdays:");
|
||||||
|
for (int count = 0; count <= 21; count++) // cover 21 days total (7 prior, current day, 14 upcoming)
|
||||||
|
{
|
||||||
|
var results = from item in query
|
||||||
|
where item.DateIndex == search
|
||||||
|
select item;
|
||||||
|
|
||||||
|
// push up search by 1 now, in case we back out early
|
||||||
|
search += 1;
|
||||||
|
if (search > 366) search = 1; // wrap to beginning of year
|
||||||
|
|
||||||
|
if (!results.Any()) continue; // back out early
|
||||||
|
resultCount += results.Count();
|
||||||
|
|
||||||
|
// Build sorted name list
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var item in results) {
|
||||||
|
names.Add(item.DisplayName);
|
||||||
|
}
|
||||||
|
names.Sort(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var first = true;
|
||||||
|
output.AppendLine();
|
||||||
|
output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
|
||||||
|
foreach (var item in names) {
|
||||||
|
// If the output is starting to fill up, send out this message and prepare a new one.
|
||||||
|
if (output.Length > 800) {
|
||||||
|
await reqChannel.SendMessageAsync(output.ToString()).ConfigureAwait(false);
|
||||||
|
output.Clear();
|
||||||
|
first = true;
|
||||||
|
output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first) first = false;
|
||||||
|
else output.Append(", ");
|
||||||
|
output.Append(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultCount == 0)
|
||||||
|
await reqChannel.SendMessageAsync(
|
||||||
|
"There are no recent or upcoming birthdays (within the last 7 days and/or next 21 days).")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
await reqChannel.SendMessageAsync(output.ToString()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches all guild birthdays and places them into an easily usable structure.
|
||||||
|
/// Users currently not in the guild are not included in the result.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<ListItem>> GetSortedUsersAsync(SocketGuild guild) {
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserConfiguration.BackingTable
|
||||||
|
+ " where guild_id = @Gid order by birth_month, birth_day";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
||||||
|
c.Prepare();
|
||||||
|
using var r = await c.ExecuteReaderAsync();
|
||||||
|
var result = new List<ListItem>();
|
||||||
|
while (await r.ReadAsync()) {
|
||||||
|
var id = (ulong)r.GetInt64(0);
|
||||||
|
var month = r.GetInt32(1);
|
||||||
|
var day = r.GetInt32(2);
|
||||||
|
|
||||||
|
var guildUser = guild.GetUser(id);
|
||||||
|
if (guildUser == null) continue; // Skip user not in guild
|
||||||
|
|
||||||
|
result.Add(new ListItem() {
|
||||||
|
BirthMonth = month,
|
||||||
|
BirthDay = day,
|
||||||
|
DateIndex = DateIndex(month, day),
|
||||||
|
UserId = guildUser.Id,
|
||||||
|
DisplayName = Common.FormatName(guildUser, false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ListExportNormal(SocketGuildChannel channel, IEnumerable<ListItem> list) {
|
||||||
|
// Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]"
|
||||||
|
var result = new StringBuilder();
|
||||||
|
result.AppendLine("Birthdays in " + channel.Guild.Name);
|
||||||
|
result.AppendLine();
|
||||||
|
foreach (var item in list) {
|
||||||
|
var user = channel.Guild.GetUser(item.UserId);
|
||||||
|
if (user == null) continue; // User disappeared in the instant between getting list and processing
|
||||||
|
result.Append($"● {Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}: ");
|
||||||
|
result.Append(item.UserId);
|
||||||
|
result.Append(" " + user.Username + "#" + user.Discriminator);
|
||||||
|
if (user.Nickname != null) result.Append(" - Nickname: " + user.Nickname);
|
||||||
|
result.AppendLine();
|
||||||
|
}
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ListExportCsv(SocketGuildChannel channel, IEnumerable<ListItem> list) {
|
||||||
|
// Output: User ID, Username, Nickname, Month-Day, Month, Day
|
||||||
|
var result = new StringBuilder();
|
||||||
|
|
||||||
|
// Conforming to RFC 4180; with header
|
||||||
|
result.Append("UserId,Username,Nickname,MonthDayDisp,Month,Day");
|
||||||
|
result.Append("\r\n"); // crlf line break is specified by the standard
|
||||||
|
foreach (var item in list) {
|
||||||
|
var user = channel.Guild.GetUser(item.UserId);
|
||||||
|
if (user == null) continue; // User disappeared in the instant between getting list and processing
|
||||||
|
result.Append(item.UserId);
|
||||||
|
result.Append(',');
|
||||||
|
result.Append(CsvEscape(user.Username + "#" + user.Discriminator));
|
||||||
|
result.Append(',');
|
||||||
|
if (user.Nickname != null) result.Append(user.Nickname);
|
||||||
|
result.Append(',');
|
||||||
|
result.Append($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}");
|
||||||
|
result.Append(',');
|
||||||
|
result.Append(item.BirthMonth);
|
||||||
|
result.Append(',');
|
||||||
|
result.Append(item.BirthDay);
|
||||||
|
result.Append("\r\n");
|
||||||
|
}
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CsvEscape(string input) {
|
||||||
|
var result = new StringBuilder();
|
||||||
|
result.Append('"');
|
||||||
|
foreach (var ch in input) {
|
||||||
|
if (ch == '"') result.Append('"');
|
||||||
|
result.Append(ch);
|
||||||
|
}
|
||||||
|
result.Append('"');
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DateIndex(int month, int day) {
|
||||||
|
var dateindex = 0;
|
||||||
|
// Add month offsets
|
||||||
|
if (month > 1) dateindex += 31; // Offset January
|
||||||
|
if (month > 2) dateindex += 29; // Offset February (incl. leap day)
|
||||||
|
if (month > 3) dateindex += 31; // etc
|
||||||
|
if (month > 4) dateindex += 30;
|
||||||
|
if (month > 5) dateindex += 31;
|
||||||
|
if (month > 6) dateindex += 30;
|
||||||
|
if (month > 7) dateindex += 31;
|
||||||
|
if (month > 8) dateindex += 31;
|
||||||
|
if (month > 9) dateindex += 30;
|
||||||
|
if (month > 10) dateindex += 31;
|
||||||
|
if (month > 11) dateindex += 30;
|
||||||
|
dateindex += day;
|
||||||
|
return dateindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ListItem {
|
||||||
|
public int DateIndex;
|
||||||
|
public int BirthMonth;
|
||||||
|
public int BirthDay;
|
||||||
|
public ulong UserId;
|
||||||
|
public string DisplayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
using Discord.WebSocket;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.UserInterface;
|
namespace BirthdayBot.UserInterface;
|
||||||
|
|
||||||
|
@ -327,9 +322,8 @@ internal class ManagerCommands : CommandsCommon {
|
||||||
// Moderators only. As with config, silently drop if this check fails.
|
// Moderators only. As with config, silently drop if this check fails.
|
||||||
if (!gconf.IsBotModerator(reqUser)) return;
|
if (!gconf.IsBotModerator(reqUser)) return;
|
||||||
|
|
||||||
if (!Common.HasMostMembersDownloaded(reqChannel.Guild)) {
|
if (!await HasMemberCacheAsync(reqChannel.Guild)) {
|
||||||
instance.RequestDownloadUsers(reqChannel.Guild.Id);
|
await reqChannel.SendMessageAsync(MemberCacheEmptyError);
|
||||||
await reqChannel.SendMessageAsync(UsersNotDownloadedError);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,7 +387,7 @@ internal class ManagerCommands : CommandsCommon {
|
||||||
var conf = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
var conf = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
||||||
|
|
||||||
result.AppendLine($"Server ID: {guild.Id} | Bot shard ID: {instance.ShardId:00}");
|
result.AppendLine($"Server ID: {guild.Id} | Bot shard ID: {instance.ShardId:00}");
|
||||||
bool hasMembers = Common.HasMostMembersDownloaded(guild);
|
bool hasMembers = guild.HasAllMembers;
|
||||||
result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers));
|
result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers));
|
||||||
result.AppendLine($" - Has {guild.DownloadedMemberCount} of {guild.MemberCount} members.");
|
result.AppendLine($" - Has {guild.DownloadedMemberCount} of {guild.MemberCount} members.");
|
||||||
int bdayCount = -1;
|
int bdayCount = -1;
|
||||||
|
|
Loading…
Reference in a new issue