mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-22 05:54:36 +00:00
272 lines
11 KiB
C#
272 lines
11 KiB
C#
|
using BirthdayBot.Data;
|
|||
|
using Discord.WebSocket;
|
|||
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.IO;
|
|||
|
using System.Linq;
|
|||
|
using System.Text;
|
|||
|
using System.Threading.Tasks;
|
|||
|
|
|||
|
namespace BirthdayBot.UserInterface
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// Commands for listing upcoming and all birthdays.
|
|||
|
/// </summary>
|
|||
|
internal class ListingCommands : CommandsCommon
|
|||
|
{
|
|||
|
public ListingCommands(BirthdayBot inst, Configuration db) : base(inst, db) { }
|
|||
|
|
|||
|
public override IEnumerable<(string, CommandHandler)> Commands
|
|||
|
=> new List<(string, CommandHandler)>()
|
|||
|
{
|
|||
|
("list", CmdList),
|
|||
|
("upcoming", CmdUpcoming),
|
|||
|
("recent", CmdUpcoming)
|
|||
|
};
|
|||
|
|
|||
|
// Creates a file with all birthdays.
|
|||
|
private async Task CmdList(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
|
|||
|
{
|
|||
|
// 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))
|
|||
|
{
|
|||
|
await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.");
|
|||
|
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.");
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
else if (param.Length > 2)
|
|||
|
{
|
|||
|
await reqChannel.SendMessageAsync(GenericError);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var bdlist = await LoadList(reqChannel.Guild, 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);
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
await reqChannel.SendFileAsync(filepath, $"Exported {bdlist.Count} birthdays to file.");
|
|||
|
}
|
|||
|
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(":x: An internal error occurred. It has been reported to the bot owner.").Wait();
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
File.Delete(filepath);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "Recent and upcoming birthdays"
|
|||
|
// 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)
|
|||
|
{
|
|||
|
var now = DateTimeOffset.UtcNow;
|
|||
|
var search = DateIndex(now.Month, now.Day) - 4; // begin search 4 days prior to current date UTC
|
|||
|
if (search <= 0) search = 366 - Math.Abs(search);
|
|||
|
|
|||
|
var query = await LoadList(reqChannel.Guild, true);
|
|||
|
|
|||
|
var output = new StringBuilder();
|
|||
|
var resultCount = 0;
|
|||
|
output.AppendLine("Recent and upcoming birthdays:");
|
|||
|
for (int count = 0; count <= 11; count++) // cover 11 days total (3 prior, current day, 7 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.Count() == 0) 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.ToString("00")}`: ");
|
|||
|
foreach (var item in names)
|
|||
|
{
|
|||
|
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 3 days and/or next 7 days).");
|
|||
|
else
|
|||
|
await reqChannel.SendMessageAsync(output.ToString());
|
|||
|
}
|
|||
|
|
|||
|
/// <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 async Task<List<ListItem>> LoadList(SocketGuild guild, bool escapeFormat)
|
|||
|
{
|
|||
|
var ping = Instance.GuildCache[guild.Id].AnnouncePing;
|
|||
|
|
|||
|
using (var db = await BotConfig.DatabaseSettings.OpenConnectionAsync())
|
|||
|
{
|
|||
|
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";
|
|||
|
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 users 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.ToString("00")}");
|
|||
|
result.Append(',');
|
|||
|
result.Append(item.BirthMonth);
|
|||
|
result.Append(',');
|
|||
|
result.Append(item.BirthDay);
|
|||
|
result.Append("\r\n");
|
|||
|
}
|
|||
|
return result.ToString();
|
|||
|
}
|
|||
|
|
|||
|
private 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 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;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|