mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-22 13:54:36 +00:00
Merge pull request #20 from NoiTheCat/dev/database-revamp
Implement Entity Framework for all database usage
This commit is contained in:
commit
8fb7ee91a3
31 changed files with 936 additions and 351 deletions
|
@ -41,8 +41,13 @@ public class BirthdayModule : BotModuleBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await ((SocketGuildUser)Context.User).GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
await user.UpdateAsync(inmonth, inday, inzone ?? user.TimeZone).ConfigureAwait(false);
|
var user = ((SocketGuildUser)Context.User).GetUserEntryOrNew(db);
|
||||||
|
if (user.IsNew) db.UserEntries.Add(user);
|
||||||
|
user.BirthMonth = inmonth;
|
||||||
|
user.BirthDay = inday;
|
||||||
|
user.TimeZone = inzone;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await RespondAsync($":white_check_mark: Your birthday has been set to **{FormatDate(inmonth, inday)}**" +
|
await RespondAsync($":white_check_mark: Your birthday has been set to **{FormatDate(inmonth, inday)}**" +
|
||||||
(inzone == null ? "" : $", with time zone {inzone}") + ".").ConfigureAwait(false);
|
(inzone == null ? "" : $", with time zone {inzone}") + ".").ConfigureAwait(false);
|
||||||
|
@ -50,31 +55,35 @@ public class BirthdayModule : BotModuleBase {
|
||||||
|
|
||||||
[SlashCommand("timezone", HelpCmdSetZone)]
|
[SlashCommand("timezone", HelpCmdSetZone)]
|
||||||
public async Task CmdSetZone([Summary(description: HelpOptZone)] string zone) {
|
public async Task CmdSetZone([Summary(description: HelpOptZone)] string zone) {
|
||||||
var user = await ((SocketGuildUser)Context.User).GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
if (!user.IsKnown) {
|
|
||||||
|
var user = ((SocketGuildUser)Context.User).GetUserEntryOrNew(db);
|
||||||
|
if (user.IsNew) {
|
||||||
await RespondAsync(":x: You do not have a birthday set.", ephemeral: true).ConfigureAwait(false);
|
await RespondAsync(":x: You do not have a birthday set.", ephemeral: true).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string inzone;
|
string newzone;
|
||||||
try {
|
try {
|
||||||
inzone = ParseTimeZone(zone);
|
newzone = ParseTimeZone(zone);
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
await RespondAsync(e.Message, ephemeral: true).ConfigureAwait(false);
|
await RespondAsync(e.Message, ephemeral: true).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await user.UpdateAsync(user.BirthMonth, user.BirthDay, inzone).ConfigureAwait(false);
|
user.TimeZone = newzone;
|
||||||
await RespondAsync($":white_check_mark: Your time zone has been set to **{inzone}**.").ConfigureAwait(false);
|
await db.SaveChangesAsync();
|
||||||
|
await RespondAsync($":white_check_mark: Your time zone has been set to **{newzone}**.").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[SlashCommand("remove", HelpCmdRemove)]
|
[SlashCommand("remove", HelpCmdRemove)]
|
||||||
public async Task CmdRemove() {
|
public async Task CmdRemove() {
|
||||||
var user = await ((SocketGuildUser)Context.User).GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
if (user.IsKnown) {
|
var user = ((SocketGuildUser)Context.User).GetUserEntryOrNew(db);
|
||||||
await user.DeleteAsync().ConfigureAwait(false);
|
if (!user.IsNew) {
|
||||||
await RespondAsync(":white_check_mark: Your birthday in this server has been removed.")
|
db.UserEntries.Remove(user);
|
||||||
.ConfigureAwait(false);
|
await db.SaveChangesAsync();
|
||||||
|
await RespondAsync(":white_check_mark: Your birthday in this server has been removed.");
|
||||||
} else {
|
} else {
|
||||||
await RespondAsync(":white_check_mark: Your birthday is not registered.")
|
await RespondAsync(":white_check_mark: Your birthday is not registered.")
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
@ -83,12 +92,15 @@ public class BirthdayModule : BotModuleBase {
|
||||||
|
|
||||||
[SlashCommand("get", "Gets a user's birthday.")]
|
[SlashCommand("get", "Gets a user's birthday.")]
|
||||||
public async Task CmdGetBday([Summary(description: "Optional: The user's birthday to look up.")] SocketGuildUser? user = null) {
|
public async Task CmdGetBday([Summary(description: "Optional: The user's birthday to look up.")] SocketGuildUser? user = null) {
|
||||||
var self = user is null;
|
using var db = new BotDatabaseContext();
|
||||||
if (self) user = (SocketGuildUser)Context.User;
|
|
||||||
var targetdata = await user!.GetConfigAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!targetdata.IsKnown) {
|
var isSelf = user is null;
|
||||||
if (self) await RespondAsync(":x: You do not have your birthday registered.", ephemeral: true).ConfigureAwait(false);
|
if (isSelf) user = (SocketGuildUser)Context.User;
|
||||||
|
|
||||||
|
var targetdata = user!.GetUserEntryOrNew(db);
|
||||||
|
|
||||||
|
if (targetdata.IsNew) {
|
||||||
|
if (isSelf) await RespondAsync(":x: You do not have your birthday registered.", ephemeral: true).ConfigureAwait(false);
|
||||||
else await RespondAsync(":x: The given user does not have their birthday registered.", ephemeral: true).ConfigureAwait(false);
|
else await RespondAsync(":x: The given user does not have their birthday registered.", ephemeral: true).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -111,7 +123,7 @@ public class BirthdayModule : BotModuleBase {
|
||||||
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 GetSortedUsersAsync(Context.Guild).ConfigureAwait(false);
|
var query = GetSortedUserList(Context.Guild);
|
||||||
|
|
||||||
// TODO pagination instead of this workaround
|
// TODO pagination instead of this workaround
|
||||||
bool hasOutputOneLine = false;
|
bool hasOutputOneLine = false;
|
||||||
|
@ -182,7 +194,7 @@ public class BirthdayModule : BotModuleBase {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bdlist = await GetSortedUsersAsync(Context.Guild).ConfigureAwait(false);
|
var bdlist = GetSortedUserList(Context.Guild);
|
||||||
|
|
||||||
var filename = "birthdaybot-" + Context.Guild.Id;
|
var filename = "birthdaybot-" + Context.Guild.Id;
|
||||||
Stream fileoutput;
|
Stream fileoutput;
|
||||||
|
@ -201,27 +213,26 @@ public class BirthdayModule : BotModuleBase {
|
||||||
/// 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 static async Task<List<ListItem>> GetSortedUsersAsync(SocketGuild guild) {
|
private static List<ListItem> GetSortedUserList(SocketGuild guild) {
|
||||||
using var db = await Database.OpenConnectionAsync();
|
using var db = new BotDatabaseContext();
|
||||||
using var c = db.CreateCommand();
|
var query = from row in db.UserEntries
|
||||||
c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserConfiguration.BackingTable
|
where row.GuildId == (long)guild.Id
|
||||||
+ " where guild_id = @Gid order by birth_month, birth_day";
|
orderby row.BirthMonth, row.BirthDay
|
||||||
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
select new {
|
||||||
c.Prepare();
|
UserId = (ulong)row.UserId,
|
||||||
using var r = await c.ExecuteReaderAsync();
|
Month = row.BirthMonth,
|
||||||
var result = new List<ListItem>();
|
Day = row.BirthDay
|
||||||
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);
|
var result = new List<ListItem>();
|
||||||
|
foreach (var row in query) {
|
||||||
|
var guildUser = guild.GetUser(row.UserId);
|
||||||
if (guildUser == null) continue; // Skip user not in guild
|
if (guildUser == null) continue; // Skip user not in guild
|
||||||
|
|
||||||
result.Add(new ListItem() {
|
result.Add(new ListItem() {
|
||||||
BirthMonth = month,
|
BirthMonth = row.Month,
|
||||||
BirthDay = day,
|
BirthDay = row.Day,
|
||||||
DateIndex = DateIndex(month, day),
|
DateIndex = DateIndex(row.Month, row.Day),
|
||||||
UserId = guildUser.Id,
|
UserId = guildUser.Id,
|
||||||
DisplayName = Common.FormatName(guildUser, false)
|
DisplayName = Common.FormatName(guildUser, false)
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,8 +24,12 @@ public class BirthdayOverrideModule : BotModuleBase {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await target.GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
await user.UpdateAsync(inmonth, inday, user.TimeZone).ConfigureAwait(false);
|
var user = target.GetUserEntryOrNew(db);
|
||||||
|
if (user.IsNew) db.UserEntries.Add(user);
|
||||||
|
user.BirthMonth = inmonth;
|
||||||
|
user.BirthDay = inday;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await RespondAsync($":white_check_mark: {Common.FormatName(target, false)}'s birthday has been set to " +
|
await RespondAsync($":white_check_mark: {Common.FormatName(target, false)}'s birthday has been set to " +
|
||||||
$"**{FormatDate(inmonth, inday)}**.").ConfigureAwait(false);
|
$"**{FormatDate(inmonth, inday)}**.").ConfigureAwait(false);
|
||||||
|
@ -34,30 +38,35 @@ public class BirthdayOverrideModule : BotModuleBase {
|
||||||
[SlashCommand("set-timezone", HelpPfxModOnly + "Set a user's time zone on their behalf.")]
|
[SlashCommand("set-timezone", HelpPfxModOnly + "Set a user's time zone on their behalf.")]
|
||||||
public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)]SocketGuildUser target,
|
public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)]SocketGuildUser target,
|
||||||
[Summary(description: HelpOptZone)]string zone) {
|
[Summary(description: HelpOptZone)]string zone) {
|
||||||
var user = await target.GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
if (!user.IsKnown) {
|
|
||||||
|
var user = target.GetUserEntryOrNew(db);
|
||||||
|
if (user.IsNew) {
|
||||||
await RespondAsync($":x: {Common.FormatName(target, false)} does not have a birthday set.")
|
await RespondAsync($":x: {Common.FormatName(target, false)} does not have a birthday set.")
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string inzone;
|
string newzone;
|
||||||
try {
|
try {
|
||||||
inzone = ParseTimeZone(zone);
|
newzone = ParseTimeZone(zone);
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
await RespondAsync(e.Message, ephemeral: true).ConfigureAwait(false);
|
await RespondAsync(e.Message, ephemeral: true).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await user.UpdateAsync(user.BirthMonth, user.BirthDay, inzone).ConfigureAwait(false);
|
user.TimeZone = newzone;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
await RespondAsync($":white_check_mark: {Common.FormatName(target, false)}'s time zone has been set to " +
|
await RespondAsync($":white_check_mark: {Common.FormatName(target, false)}'s time zone has been set to " +
|
||||||
$"**{inzone}**.").ConfigureAwait(false);
|
$"**{newzone}**.").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SlashCommand("remove-birthday", HelpPfxModOnly + "Remove a user's birthday information on their behalf.")]
|
[SlashCommand("remove-birthday", HelpPfxModOnly + "Remove a user's birthday information on their behalf.")]
|
||||||
public async Task OvRemove([Summary(description: HelpOptOvTarget)]SocketGuildUser target) {
|
public async Task OvRemove([Summary(description: HelpOptOvTarget)]SocketGuildUser target) {
|
||||||
var user = await target.GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
if (user.IsKnown) {
|
var user = ((SocketGuildUser)Context.User).GetUserEntryOrNew(db);
|
||||||
await user.DeleteAsync().ConfigureAwait(false);
|
if (!user.IsNew) {
|
||||||
|
db.UserEntries.Remove(user);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
await RespondAsync($":white_check_mark: {Common.FormatName(target, false)}'s birthday in this server has been removed.")
|
await RespondAsync($":white_check_mark: {Common.FormatName(target, false)}'s birthday in this server has been removed.")
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -57,16 +57,15 @@ public class ConfigModule : BotModuleBase {
|
||||||
|
|
||||||
[SlashCommand("set-channel", HelpPfxModOnly + HelpSubCmdChannel + HelpPofxBlankUnset)]
|
[SlashCommand("set-channel", HelpPfxModOnly + HelpSubCmdChannel + HelpPofxBlankUnset)]
|
||||||
public async Task CmdSetChannel([Summary(description: HelpOptRole)] SocketTextChannel? channel = null) {
|
public async Task CmdSetChannel([Summary(description: HelpOptRole)] SocketTextChannel? channel = null) {
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
await DoDatabaseUpdate(Context, s => s.ChannelAnnounceId = (long?)channel?.Id);
|
||||||
gconf.AnnounceChannelId = channel?.Id;
|
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
|
||||||
await RespondAsync(":white_check_mark: The announcement channel has been " +
|
await RespondAsync(":white_check_mark: The announcement channel has been " +
|
||||||
(channel == null ? "unset." : $"set to **{channel.Name}**."));
|
(channel == null ? "unset." : $"set to **{channel.Name}**."));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SlashCommand("set-message", HelpPfxModOnly + HelpSubCmdMessage)]
|
[SlashCommand("set-message", HelpPfxModOnly + HelpSubCmdMessage)]
|
||||||
public async Task CmdSetMessage() {
|
public async Task CmdSetMessage() {
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
|
var settings = Context.Guild.GetConfigOrNew(db);
|
||||||
|
|
||||||
var txtSingle = new TextInputBuilder() {
|
var txtSingle = new TextInputBuilder() {
|
||||||
Label = "Single - Message for one birthday",
|
Label = "Single - Message for one birthday",
|
||||||
|
@ -75,7 +74,7 @@ public class ConfigModule : BotModuleBase {
|
||||||
MaxLength = 1500,
|
MaxLength = 1500,
|
||||||
Required = false,
|
Required = false,
|
||||||
Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnounce,
|
Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnounce,
|
||||||
Value = gconf.AnnounceMessages.Item1 ?? ""
|
Value = settings.AnnounceMessage ?? ""
|
||||||
};
|
};
|
||||||
var txtMulti = new TextInputBuilder() {
|
var txtMulti = new TextInputBuilder() {
|
||||||
Label = "Multi - Message for multiple birthdays",
|
Label = "Multi - Message for multiple birthdays",
|
||||||
|
@ -84,7 +83,7 @@ public class ConfigModule : BotModuleBase {
|
||||||
MaxLength = 1500,
|
MaxLength = 1500,
|
||||||
Required = false,
|
Required = false,
|
||||||
Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnouncePl,
|
Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnouncePl,
|
||||||
Value = gconf.AnnounceMessages.Item2 ?? ""
|
Value = settings.AnnounceMessagePl ?? ""
|
||||||
};
|
};
|
||||||
|
|
||||||
var form = new ModalBuilder()
|
var form = new ModalBuilder()
|
||||||
|
@ -104,18 +103,18 @@ public class ConfigModule : BotModuleBase {
|
||||||
if (string.IsNullOrWhiteSpace(newSingle)) newSingle = null;
|
if (string.IsNullOrWhiteSpace(newSingle)) newSingle = null;
|
||||||
if (string.IsNullOrWhiteSpace(newMulti)) newMulti = null;
|
if (string.IsNullOrWhiteSpace(newMulti)) newMulti = null;
|
||||||
|
|
||||||
var gconf = await channel.Guild.GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
gconf.AnnounceMessages = (newSingle, newMulti);
|
var settings = channel.Guild.GetConfigOrNew(db);
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
if (settings.IsNew) db.GuildConfigurations.Add(settings);
|
||||||
|
settings.AnnounceMessage = newSingle;
|
||||||
|
settings.AnnounceMessagePl = newMulti;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
await modal.RespondAsync(":white_check_mark: Announcement messages have been updated.");
|
await modal.RespondAsync(":white_check_mark: Announcement messages have been updated.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[SlashCommand("set-ping", HelpPfxModOnly + HelpSubCmdPing)]
|
[SlashCommand("set-ping", HelpPfxModOnly + HelpSubCmdPing)]
|
||||||
public async Task CmdSetPing([Summary(description: "Set True to ping users, False to display them normally.")]bool option) {
|
public async Task CmdSetPing([Summary(description: "Set True to ping users, False to display them normally.")]bool option) {
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
await DoDatabaseUpdate(Context, s => s.AnnouncePing = option);
|
||||||
gconf.AnnouncePing = option;
|
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
|
||||||
await RespondAsync($":white_check_mark: Announcement pings are now **{(option ? "on" : "off")}**.").ConfigureAwait(false);
|
await RespondAsync($":white_check_mark: Announcement pings are now **{(option ? "on" : "off")}**.").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,17 +123,13 @@ public class ConfigModule : BotModuleBase {
|
||||||
public class SubCmdsConfigRole : BotModuleBase {
|
public class SubCmdsConfigRole : BotModuleBase {
|
||||||
[SlashCommand("set-birthday-role", HelpPfxModOnly + "Set the role given to users having a birthday.")]
|
[SlashCommand("set-birthday-role", HelpPfxModOnly + "Set the role given to users having a birthday.")]
|
||||||
public async Task CmdSetBRole([Summary(description: HelpOptRole)] SocketRole role) {
|
public async Task CmdSetBRole([Summary(description: HelpOptRole)] SocketRole role) {
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
await DoDatabaseUpdate(Context, s => s.RoleId = (long)role.Id);
|
||||||
gconf.RoleId = role.Id;
|
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
|
||||||
await RespondAsync($":white_check_mark: The birthday role has been set to **{role.Name}**.").ConfigureAwait(false);
|
await RespondAsync($":white_check_mark: The birthday role has been set to **{role.Name}**.").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SlashCommand("set-moderator-role", HelpPfxModOnly + "Designate a role whose members can configure the bot." + HelpPofxBlankUnset)]
|
[SlashCommand("set-moderator-role", HelpPfxModOnly + "Designate a role whose members can configure the bot." + HelpPofxBlankUnset)]
|
||||||
public async Task CmdSetModRole([Summary(description: HelpOptRole)]SocketRole? role = null) {
|
public async Task CmdSetModRole([Summary(description: HelpOptRole)]SocketRole? role = null) {
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
await DoDatabaseUpdate(Context, s => s.ModeratorRole = (long?)role?.Id);
|
||||||
gconf.ModeratorRole = role?.Id;
|
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
|
||||||
await RespondAsync(":white_check_mark: The moderator role has been " +
|
await RespondAsync(":white_check_mark: The moderator role has been " +
|
||||||
(role == null ? "unset." : $"set to **{role.Name}**."));
|
(role == null ? "unset." : $"set to **{role.Name}**."));
|
||||||
}
|
}
|
||||||
|
@ -149,26 +144,36 @@ public class ConfigModule : BotModuleBase {
|
||||||
public Task CmdDelBlock([Summary(description: "The user to unblock.")] SocketGuildUser user) => UpdateBlockAsync(user, false);
|
public Task CmdDelBlock([Summary(description: "The user to unblock.")] SocketGuildUser user) => UpdateBlockAsync(user, false);
|
||||||
|
|
||||||
private async Task UpdateBlockAsync(SocketGuildUser user, bool setting) {
|
private async Task UpdateBlockAsync(SocketGuildUser user, bool setting) {
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
// setting: true to add (set), false to remove (unset)
|
||||||
bool already = setting == await gconf.IsUserBlockedAsync(user.Id).ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
|
var existing = db.BlocklistEntries
|
||||||
|
.Where(bl => bl.GuildId == (long)user.Guild.Id && bl.UserId == (long)user.Id).FirstOrDefault();
|
||||||
|
|
||||||
|
bool already = (existing != null) == setting;
|
||||||
if (already) {
|
if (already) {
|
||||||
await RespondAsync($":white_check_mark: User is already {(setting ? "" : "not ")}blocked.").ConfigureAwait(false);
|
await RespondAsync($":white_check_mark: User is already {(setting ? "" : "not ")}blocked.").ConfigureAwait(false);
|
||||||
} else {
|
return;
|
||||||
if (setting) await gconf.BlockUserAsync(user.Id).ConfigureAwait(false);
|
|
||||||
else await gconf.UnblockUserAsync(user.Id).ConfigureAwait(false);
|
|
||||||
await RespondAsync($":white_check_mark: {Common.FormatName(user, false)} has been {(setting ? "" : "un")}blocked.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting) db.BlocklistEntries.Add(new BlocklistEntry() { GuildId = (long)user.Guild.Id, UserId = (long)user.Id });
|
||||||
|
else db.Remove(existing!);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await RespondAsync($":white_check_mark: {Common.FormatName(user, false)} has been {(setting ? "" : "un")}blocked.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[SlashCommand("set-moderated", HelpPfxModOnly + "Set moderated mode on the server.")]
|
[SlashCommand("set-moderated", HelpPfxModOnly + "Set moderated mode on the server.")]
|
||||||
public async Task CmdAddBlock([Summary(name: "enable", description: "The moderated mode setting.")] bool setting) {
|
public async Task CmdSetModerated([Summary(name: "enable", description: "The moderated mode setting.")] bool setting) {
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
bool current = false;
|
||||||
bool already = setting == gconf.IsModerated;
|
await DoDatabaseUpdate(Context, s => {
|
||||||
|
current = s.Moderated;
|
||||||
|
s.Moderated = setting;
|
||||||
|
});
|
||||||
|
|
||||||
|
bool already = setting == current;
|
||||||
if (already) {
|
if (already) {
|
||||||
await RespondAsync($":white_check_mark: Moderated mode is already **{(setting ? "en" : "dis")}abled**.").ConfigureAwait(false);
|
await RespondAsync($":white_check_mark: Moderated mode is already **{(setting ? "en" : "dis")}abled**.");
|
||||||
} else {
|
} else {
|
||||||
gconf.IsModerated = setting;
|
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
|
||||||
await RespondAsync($":white_check_mark: Moderated mode is now **{(setting ? "en" : "dis")}abled**.").ConfigureAwait(false);
|
await RespondAsync($":white_check_mark: Moderated mode is now **{(setting ? "en" : "dis")}abled**.").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,15 +182,19 @@ public class ConfigModule : BotModuleBase {
|
||||||
[SlashCommand("check", HelpPfxModOnly + HelpCmdCheck)]
|
[SlashCommand("check", HelpPfxModOnly + HelpCmdCheck)]
|
||||||
public async Task CmdCheck() {
|
public async Task CmdCheck() {
|
||||||
static string DoTestFor(string label, Func<bool> test) => $"{label}: { (test() ? ":white_check_mark: Yes" : ":x: No") }";
|
static string DoTestFor(string label, Func<bool> test) => $"{label}: { (test() ? ":white_check_mark: Yes" : ":x: No") }";
|
||||||
var result = new StringBuilder();
|
|
||||||
SocketTextChannel channel = (SocketTextChannel)Context.Channel;
|
SocketTextChannel channel = (SocketTextChannel)Context.Channel;
|
||||||
var guild = Context.Guild;
|
var guild = Context.Guild;
|
||||||
var conf = await guild.GetConfigAsync().ConfigureAwait(false);
|
|
||||||
var usercfgs = await guild.GetUserConfigurationsAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
|
var guildconf = guild.GetConfigOrNew(db);
|
||||||
|
await db.Entry(guildconf).Collection(t => t.UserEntries).LoadAsync();
|
||||||
|
|
||||||
|
var result = new StringBuilder();
|
||||||
|
|
||||||
result.AppendLine($"Server ID: `{guild.Id}` | Bot shard ID: `{Shard.ShardId:00}`");
|
result.AppendLine($"Server ID: `{guild.Id}` | Bot shard ID: `{Shard.ShardId:00}`");
|
||||||
result.AppendLine($"Number of registered birthdays: `{ usercfgs.Count() }`");
|
result.AppendLine($"Number of registered birthdays: `{ guildconf.UserEntries.Count() }`");
|
||||||
result.AppendLine($"Server time zone: `{ (conf?.TimeZone ?? "Not set - using UTC") }`");
|
result.AppendLine($"Server time zone: `{ (guildconf.TimeZone ?? "Not set - using UTC") }`");
|
||||||
result.AppendLine();
|
result.AppendLine();
|
||||||
|
|
||||||
bool hasMembers = Common.HasMostMembersDownloaded(guild);
|
bool hasMembers = Common.HasMostMembersDownloaded(guild);
|
||||||
|
@ -194,7 +203,7 @@ public class ConfigModule : BotModuleBase {
|
||||||
int bdayCount = -1;
|
int bdayCount = -1;
|
||||||
result.Append(DoTestFor("Birthday processing", delegate {
|
result.Append(DoTestFor("Birthday processing", delegate {
|
||||||
if (!hasMembers) return false;
|
if (!hasMembers) return false;
|
||||||
bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(usercfgs, conf?.TimeZone).Count;
|
bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(guildconf.UserEntries, guildconf.TimeZone).Count;
|
||||||
return true;
|
return true;
|
||||||
}));
|
}));
|
||||||
if (hasMembers) result.AppendLine($" - `{bdayCount}` user(s) currently having a birthday.");
|
if (hasMembers) result.AppendLine($" - `{bdayCount}` user(s) currently having a birthday.");
|
||||||
|
@ -202,13 +211,13 @@ public class ConfigModule : BotModuleBase {
|
||||||
result.AppendLine();
|
result.AppendLine();
|
||||||
|
|
||||||
result.AppendLine(DoTestFor("Birthday role set with `bb.config role`", delegate {
|
result.AppendLine(DoTestFor("Birthday role set with `bb.config role`", delegate {
|
||||||
if (conf == null) return false;
|
if (guildconf.IsNew) return false;
|
||||||
SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
|
SocketRole? role = guild.GetRole((ulong)(guildconf.RoleId ?? 0));
|
||||||
return role != null;
|
return role != null;
|
||||||
}));
|
}));
|
||||||
result.AppendLine(DoTestFor("Birthday role can be managed by bot", delegate {
|
result.AppendLine(DoTestFor("Birthday role can be managed by bot", delegate {
|
||||||
if (conf == null) return false;
|
if (guildconf.IsNew) return false;
|
||||||
SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
|
SocketRole? role = guild.GetRole((ulong)(guildconf.RoleId ?? 0));
|
||||||
if (role == null) return false;
|
if (role == null) return false;
|
||||||
return guild.CurrentUser.GuildPermissions.ManageRoles && role.Position < guild.CurrentUser.Hierarchy;
|
return guild.CurrentUser.GuildPermissions.ManageRoles && role.Position < guild.CurrentUser.Hierarchy;
|
||||||
}));
|
}));
|
||||||
|
@ -216,8 +225,8 @@ public class ConfigModule : BotModuleBase {
|
||||||
|
|
||||||
SocketTextChannel? announcech = null;
|
SocketTextChannel? announcech = null;
|
||||||
result.AppendLine(DoTestFor("(Optional) Announcement channel set with `bb.config channel`", delegate {
|
result.AppendLine(DoTestFor("(Optional) Announcement channel set with `bb.config channel`", delegate {
|
||||||
if (conf == null) return false;
|
if (guildconf.IsNew) return false;
|
||||||
announcech = guild.GetTextChannel(conf.AnnounceChannelId ?? 0);
|
announcech = guild.GetTextChannel((ulong)(guildconf.ChannelAnnounceId ?? 0));
|
||||||
return announcech != null;
|
return announcech != null;
|
||||||
}));
|
}));
|
||||||
string disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>";
|
string disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>";
|
||||||
|
@ -239,14 +248,14 @@ public class ConfigModule : BotModuleBase {
|
||||||
result.AppendLine($"> {line}");
|
result.AppendLine($"> {line}");
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
if (conf != null && (conf.AnnounceMessages.Item1 != null || conf.AnnounceMessages.Item2 != null)) {
|
if (!guildconf.IsNew && (guildconf.AnnounceMessage != null || guildconf.AnnounceMessagePl != null)) {
|
||||||
var em = new EmbedBuilder().WithAuthor(new EmbedAuthorBuilder() { Name = "Custom announce messages:" });
|
var em = new EmbedBuilder().WithAuthor(new EmbedAuthorBuilder() { Name = "Custom announce messages:" });
|
||||||
var dispAnnounces = new StringBuilder("Custom announcement message(s):\n");
|
var dispAnnounces = new StringBuilder("Custom announcement message(s):\n");
|
||||||
if (conf.AnnounceMessages.Item1 != null) {
|
if (guildconf.AnnounceMessage != null) {
|
||||||
em = em.AddField("Single", prepareAnnouncePreview(conf.AnnounceMessages.Item1));
|
em = em.AddField("Single", prepareAnnouncePreview(guildconf.AnnounceMessage));
|
||||||
}
|
}
|
||||||
if (conf.AnnounceMessages.Item2 != null) {
|
if (guildconf.AnnounceMessagePl != null) {
|
||||||
em = em.AddField("Multi", prepareAnnouncePreview(conf.AnnounceMessages.Item2));
|
em = em.AddField("Multi", prepareAnnouncePreview(guildconf.AnnounceMessagePl));
|
||||||
}
|
}
|
||||||
await ReplyAsync(embed: em.Build()).ConfigureAwait(false);
|
await ReplyAsync(embed: em.Build()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -255,11 +264,9 @@ public class ConfigModule : BotModuleBase {
|
||||||
[SlashCommand("set-timezone", HelpPfxModOnly + "Configure the time zone to use by default in the server." + HelpPofxBlankUnset)]
|
[SlashCommand("set-timezone", HelpPfxModOnly + "Configure the time zone to use by default in the server." + HelpPofxBlankUnset)]
|
||||||
public async Task CmdSetTimezone([Summary(description: HelpOptZone)] string? zone = null) {
|
public async Task CmdSetTimezone([Summary(description: HelpOptZone)] string? zone = null) {
|
||||||
const string Response = ":white_check_mark: The server's time zone has been ";
|
const string Response = ":white_check_mark: The server's time zone has been ";
|
||||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (zone == null) {
|
if (zone == null) {
|
||||||
gconf.TimeZone = null;
|
await DoDatabaseUpdate(Context, s => s.TimeZone = null);
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
|
||||||
await RespondAsync(Response + "unset.").ConfigureAwait(false);
|
await RespondAsync(Response + "unset.").ConfigureAwait(false);
|
||||||
} else {
|
} else {
|
||||||
string parsedZone;
|
string parsedZone;
|
||||||
|
@ -270,9 +277,22 @@ public class ConfigModule : BotModuleBase {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
gconf.TimeZone = parsedZone;
|
await DoDatabaseUpdate(Context, s => s.TimeZone = parsedZone);
|
||||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
await RespondAsync(Response + $"set to **{parsedZone}**.").ConfigureAwait(false);
|
||||||
await RespondAsync(Response + $"set to **{zone}**.").ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method for updating arbitrary <see cref="GuildConfig"/> values without all the boilerplate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="valueUpdater">A delegate which modifies <see cref="GuildConfig"/> properties as needed.</param>
|
||||||
|
private static async Task DoDatabaseUpdate(SocketInteractionContext context, Action<GuildConfig> valueUpdater) {
|
||||||
|
using var db = new BotDatabaseContext();
|
||||||
|
var settings = context.Guild.GetConfigOrNew(db);
|
||||||
|
|
||||||
|
valueUpdater(settings);
|
||||||
|
|
||||||
|
if (settings.IsNew) db.GuildConfigurations.Add(settings);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,26 +13,31 @@ class EnforceBlockingAttribute : PreconditionAttribute {
|
||||||
public const string ReplyModerated = ":x: This bot is in moderated mode, preventing you from using any bot commands in this server.";
|
public const string ReplyModerated = ":x: This bot is in moderated mode, preventing you from using any bot commands in this server.";
|
||||||
public const string ReplyBlocked = ":x: You have been blocked from using bot commands in this server.";
|
public const string ReplyBlocked = ":x: You have been blocked from using bot commands in this server.";
|
||||||
|
|
||||||
public override async Task<PreconditionResult> CheckRequirementsAsync(
|
public override Task<PreconditionResult> CheckRequirementsAsync(
|
||||||
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
|
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
|
||||||
// Not in guild context, unaffected by blocking
|
// Not in guild context, unaffected by blocking
|
||||||
if (context.Guild is not SocketGuild guild) return PreconditionResult.FromSuccess();
|
if (context.Guild is not SocketGuild guild) return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
|
||||||
// Manage Guild permission overrides any blocks
|
// Manage Guild permission overrides any blocks
|
||||||
var user = (SocketGuildUser)context.User;
|
var user = (SocketGuildUser)context.User;
|
||||||
if (user.GuildPermissions.ManageGuild) return PreconditionResult.FromSuccess();
|
if (user.GuildPermissions.ManageGuild) return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
|
||||||
var gconf = await guild.GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
|
var settings = (from row in db.GuildConfigurations
|
||||||
|
where row.GuildId == (long)guild.Id
|
||||||
|
select new { ModRole = (ulong?)row.ModeratorRole, ModMode = row.Moderated }).FirstOrDefault();
|
||||||
|
if (settings != null) {
|
||||||
|
// Bot moderators override all blocking measures in place
|
||||||
|
if (user.Roles.Any(r => r.Id == settings.ModRole)) return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
|
||||||
// Bot moderators override any blocks
|
// Check for moderated mode
|
||||||
if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value)) return PreconditionResult.FromSuccess();
|
if (settings.ModMode) return Task.FromResult(PreconditionResult.FromError(FailModerated));
|
||||||
|
|
||||||
// Moderated mode check
|
// Check if user exists in blocklist
|
||||||
if (gconf.IsModerated) return PreconditionResult.FromError(FailModerated);
|
if (db.BlocklistEntries.Where(row => row.GuildId == (long)guild.Id && row.UserId == (long)user.Id).Any())
|
||||||
|
return Task.FromResult(PreconditionResult.FromError(FailBlocked));
|
||||||
|
}
|
||||||
|
|
||||||
// Block list check
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
if (await gconf.IsUserInBlocklistAsync(user.Id)) return PreconditionResult.FromError(FailBlocked);
|
|
||||||
|
|
||||||
return PreconditionResult.FromSuccess();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,17 +13,21 @@ class RequireBotModeratorAttribute : PreconditionAttribute {
|
||||||
|
|
||||||
public override string ErrorMessage => Error;
|
public override string ErrorMessage => Error;
|
||||||
|
|
||||||
public override async Task<PreconditionResult> CheckRequirementsAsync(
|
public override Task<PreconditionResult> CheckRequirementsAsync(
|
||||||
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
|
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
|
||||||
// A bot moderator can only exist in a guild context, so we must do this check.
|
// A bot moderator can only exist in a guild context, so we must do this check.
|
||||||
// This check causes this precondition to become a functional equivalent to RequireGuildContextAttribute...
|
// This check causes this precondition to become a functional equivalent to RequireGuildContextAttribute...
|
||||||
if (context.User is not SocketGuildUser user) return PreconditionResult.FromError(RequireGuildContextAttribute.Error);
|
if (context.User is not SocketGuildUser user)
|
||||||
|
return Task.FromResult(PreconditionResult.FromError(RequireGuildContextAttribute.Error));
|
||||||
|
|
||||||
if (user.GuildPermissions.ManageGuild) return PreconditionResult.FromSuccess();
|
if (user.GuildPermissions.ManageGuild) return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
var gconf = await ((SocketGuild)context.Guild).GetConfigAsync().ConfigureAwait(false);
|
using var db = new BotDatabaseContext();
|
||||||
if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value))
|
var checkRole = (ulong?)db.GuildConfigurations
|
||||||
return PreconditionResult.FromSuccess();
|
.Where(g => g.GuildId == (long)((SocketGuild)context.Guild).Id)
|
||||||
|
.Select(g => g.RoleId).FirstOrDefault();
|
||||||
|
if (checkRole.HasValue && user.Roles.Any(r => r.Id == checkRole.Value))
|
||||||
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
|
||||||
return PreconditionResult.FromError(Error);
|
return Task.FromResult(PreconditionResult.FromError(Error));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,29 +9,25 @@ class AutoUserDownload : BackgroundService {
|
||||||
public AutoUserDownload(ShardInstance instance) : base(instance) { }
|
public AutoUserDownload(ShardInstance instance) : base(instance) { }
|
||||||
|
|
||||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
using var db = new BotDatabaseContext();
|
||||||
// Has the potential to disconnect while in the middle of processing.
|
|
||||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) return;
|
|
||||||
|
|
||||||
// Determine if there is action to be taken...
|
// Take action if a guild's cache is incomplete...
|
||||||
if (!guild.HasAllMembers && await GuildUserAnyAsync(guild.Id).ConfigureAwait(false)) {
|
var incompleteCaches = ShardInstance.DiscordClient.Guilds.Where(g => !g.HasAllMembers).Select(g => (long)g.Id).ToHashSet();
|
||||||
await guild.DownloadUsersAsync().ConfigureAwait(false); // This is already on a separate thread; no need to Task.Run
|
// ...and if the guild contains any user data
|
||||||
|
var mustFetch = db.UserEntries.Where(e => incompleteCaches.Contains(e.GuildId)).Select(e => e.GuildId).Distinct();
|
||||||
|
|
||||||
|
int processed = 0;
|
||||||
|
foreach (var item in mustFetch) {
|
||||||
|
// May cause a disconnect in certain situations. Cancel all further attempts until the next pass if it happens.
|
||||||
|
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) break;
|
||||||
|
|
||||||
|
var guild = ShardInstance.DiscordClient.GetGuild((ulong)item);
|
||||||
|
if (guild == null) continue; // A guild disappeared...?
|
||||||
|
await guild.DownloadUsersAsync().ConfigureAwait(false); // We're already on a seperate thread, no need to use Task.Run
|
||||||
await Task.Delay(200, CancellationToken.None).ConfigureAwait(false); // Must delay, or else it seems to hang...
|
await Task.Delay(200, CancellationToken.None).ConfigureAwait(false); // Must delay, or else it seems to hang...
|
||||||
}
|
processed++;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (processed > 100) Log($"Explicit user list request processed for {processed} guild(s).");
|
||||||
/// Determines if the user database contains any entries corresponding to this guild.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True if any entries exist.</returns>
|
|
||||||
private static async Task<bool> GuildUserAnyAsync(ulong guildId) {
|
|
||||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $"select true from {GuildUserConfiguration.BackingTable} where guild_id = @Gid limit 1";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guildId;
|
|
||||||
await c.PrepareAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
using var r = await c.ExecuteReaderAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
return r.Read();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
using System.Threading;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
|
||||||
|
|
||||||
abstract class BackgroundService {
|
abstract class BackgroundService {
|
||||||
protected ShardInstance ShardInstance { get; }
|
protected ShardInstance ShardInstance { get; }
|
||||||
|
|
|
@ -15,71 +15,61 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// Processes birthday updates for all available guilds synchronously.
|
/// Processes birthday updates for all available guilds synchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
var exs = new List<Exception>();
|
// For database efficiency, fetch all database information at once before proceeding
|
||||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
using var db = new BotDatabaseContext();
|
||||||
if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) {
|
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => (long)g.Id).ToHashSet();
|
||||||
Log("Client is not connected. Stopping early.");
|
var presentGuildSettings = db.GuildConfigurations.Where(s => shardGuilds.Contains(s.GuildId));
|
||||||
return;
|
var guildChecks = presentGuildSettings.ToList().Select(s => new Tuple<ulong, GuildConfig>((ulong)s.GuildId, s));
|
||||||
}
|
|
||||||
|
var exceptions = new List<Exception>();
|
||||||
|
foreach (var (guildId, settings) in guildChecks) {
|
||||||
|
var guild = ShardInstance.DiscordClient.GetGuild(guildId);
|
||||||
|
if (guild == null) continue; // A guild disappeared...?
|
||||||
|
|
||||||
// Check task cancellation here. Processing during a single guild is never interrupted.
|
// Check task cancellation here. Processing during a single guild is never interrupted.
|
||||||
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
||||||
|
|
||||||
try {
|
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) {
|
||||||
await ProcessGuildAsync(guild).ConfigureAwait(false);
|
Log("Client is not connected. Stopping early.");
|
||||||
} catch (Exception ex) {
|
|
||||||
// Catch all exceptions per-guild but continue processing, throw at end.
|
|
||||||
exs.Add(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exs.Count != 0) throw new AggregateException(exs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Main method where actual guild processing occurs.
|
|
||||||
/// </summary>
|
|
||||||
private static async Task ProcessGuildAsync(SocketGuild guild) {
|
|
||||||
// Load guild information - stop if local cache is unavailable.
|
|
||||||
if (!Common.HasMostMembersDownloaded(guild)) return;
|
|
||||||
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
|
||||||
if (gc == null) return;
|
|
||||||
|
|
||||||
// Check if role settings are correct before continuing with further processing
|
|
||||||
SocketRole? role = guild.GetRole(gc.RoleId ?? 0);
|
|
||||||
if (role == null || !guild.CurrentUser.GuildPermissions.ManageRoles || role.Position >= guild.CurrentUser.Hierarchy) return;
|
|
||||||
|
|
||||||
// Determine who's currently having a birthday
|
|
||||||
var users = await GuildUserConfiguration.LoadAllAsync(guild.Id).ConfigureAwait(false);
|
|
||||||
var tz = gc.TimeZone;
|
|
||||||
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.
|
|
||||||
|
|
||||||
IEnumerable<SocketGuildUser> announcementList;
|
|
||||||
// Update roles as appropriate
|
|
||||||
try {
|
|
||||||
var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays).ConfigureAwait(false);
|
|
||||||
announcementList = updateResult.Item1;
|
|
||||||
} catch (Discord.Net.HttpException) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Birthday announcement
|
try {
|
||||||
var announce = gc.AnnounceMessages;
|
// Verify that role settings and permissions are usable
|
||||||
var announceping = gc.AnnouncePing;
|
SocketRole? role = guild.GetRole((ulong)(settings.RoleId ?? 0));
|
||||||
SocketTextChannel? channel = null;
|
if (role == null
|
||||||
if (gc.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gc.AnnounceChannelId.Value);
|
|| !guild.CurrentUser.GuildPermissions.ManageRoles
|
||||||
|
|| role.Position >= guild.CurrentUser.Hierarchy) continue;
|
||||||
|
|
||||||
|
// Load up user configs and begin processing birthdays
|
||||||
|
await db.Entry(settings).Collection(t => t.UserEntries).LoadAsync(CancellationToken.None);
|
||||||
|
var birthdays = GetGuildCurrentBirthdays(settings.UserEntries, settings.TimeZone);
|
||||||
|
|
||||||
|
// Add or remove roles as appropriate
|
||||||
|
var announcementList = await UpdateGuildBirthdayRoles(guild, role, birthdays);
|
||||||
|
|
||||||
|
// Process birthday announcement
|
||||||
if (announcementList.Any()) {
|
if (announcementList.Any()) {
|
||||||
await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList).ConfigureAwait(false);
|
await AnnounceBirthdaysAsync(settings, guild, announcementList);
|
||||||
}
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Catch all exceptions per-guild but continue processing, throw at end.
|
||||||
|
exceptions.Add(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exceptions.Count > 1) throw new AggregateException("Unhandled exceptions occurred when processing birthdays.", exceptions);
|
||||||
|
else if (exceptions.Count == 1) throw new Exception("An unhandled exception occurred when processing a birthday.", exceptions[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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>
|
||||||
|
[Obsolete(Database.ObsoleteReason)]
|
||||||
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string? defaultTzStr) {
|
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string? defaultTzStr) {
|
||||||
var tzdb = DateTimeZoneProviders.Tzdb;
|
var tzdb = DateTimeZoneProviders.Tzdb;
|
||||||
DateTimeZone defaultTz = (defaultTzStr != null ? DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr) : null) ?? tzdb.GetZoneOrNull("UTC")!;
|
DateTimeZone defaultTz = (defaultTzStr != null ? DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr) : null)
|
||||||
|
?? tzdb.GetZoneOrNull("UTC")!;
|
||||||
|
|
||||||
var birthdayUsers = new HashSet<ulong>();
|
var birthdayUsers = new HashSet<ulong>();
|
||||||
foreach (var item in guildUsers) {
|
foreach (var item in guildUsers) {
|
||||||
|
@ -101,39 +91,64 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
}
|
}
|
||||||
return birthdayUsers;
|
return birthdayUsers;
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<UserEntry> guildUsers, string? ServerDefaultTzId) {
|
||||||
|
var birthdayUsers = new HashSet<ulong>();
|
||||||
|
|
||||||
|
foreach (var record in guildUsers) {
|
||||||
|
// Determine final time zone to use for calculation
|
||||||
|
DateTimeZone tz = DateTimeZoneProviders.Tzdb
|
||||||
|
.GetZoneOrNull(record.TimeZone ?? ServerDefaultTzId ?? "UTC")!;
|
||||||
|
|
||||||
|
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||||
|
// Special case: If user's birthday is 29-Feb and it's currently not a leap year, check against 1-Mar
|
||||||
|
if (!DateTime.IsLeapYear(checkNow.Year) && record.BirthMonth == 2 && record.BirthDay == 29) {
|
||||||
|
if (checkNow.Month == 3 && checkNow.Day == 1) birthdayUsers.Add((ulong)record.UserId);
|
||||||
|
} else if (record.BirthMonth == checkNow.Month && record.BirthDay== checkNow.Day) {
|
||||||
|
birthdayUsers.Add((ulong)record.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return birthdayUsers;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the birthday role to all applicable users. Unsets it from all others who may have it.
|
/// Sets the birthday role to all applicable users. Unsets it from all others who may have it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// First item: List of users who had the birthday role applied, used to announce.
|
/// List of users who had the birthday role applied, used to announce.
|
||||||
/// Second item: Counts of users who have had roles added/removed, used for operation reporting.
|
|
||||||
/// </returns>
|
/// </returns>
|
||||||
private static async Task<(IEnumerable<SocketGuildUser>, (int, int))> UpdateGuildBirthdayRoles(
|
private static async Task<IEnumerable<SocketGuildUser>> UpdateGuildBirthdayRoles(SocketGuild g, SocketRole r, HashSet<ulong> toApply) {
|
||||||
SocketGuild g, SocketRole r, HashSet<ulong> names) {
|
var additions = new List<SocketGuildUser>();
|
||||||
// Check members currently with the role. Figure out which users to remove it from.
|
try {
|
||||||
var roleRemoves = new List<SocketGuildUser>();
|
var removals = new List<SocketGuildUser>(); // TODO check if roles can be removed in-place instead of building a list first
|
||||||
var roleKeeps = new HashSet<ulong>();
|
var no_ops = new HashSet<ulong>();
|
||||||
foreach (var member in r.Members) {
|
|
||||||
if (!names.Contains(member.Id)) roleRemoves.Add(member);
|
// Scan role for members no longer needing it
|
||||||
else roleKeeps.Add(member.Id);
|
foreach (var user in r.Members) {
|
||||||
|
if (!toApply.Contains(user.Id)) removals.Add(user);
|
||||||
|
else no_ops.Add(user.Id);
|
||||||
|
}
|
||||||
|
foreach (var user in removals) {
|
||||||
|
// TODO this gets hit with rate limits sometimes. figure something out.
|
||||||
|
await user.RemoveRoleAsync(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var user in roleRemoves) {
|
foreach (var target in toApply) {
|
||||||
await user.RemoveRoleAsync(r).ConfigureAwait(false);
|
if (no_ops.Contains(target)) continue;
|
||||||
|
var user = g.GetUser(target);
|
||||||
|
if (user == null) continue; // User existing in database but not in guild
|
||||||
|
await user.AddRoleAsync(r);
|
||||||
|
additions.Add(user);
|
||||||
}
|
}
|
||||||
|
} catch (Discord.Net.HttpException ex)
|
||||||
// Apply role to members not already having it. Prepare announcement list.
|
when (ex.DiscordCode is DiscordErrorCode.MissingPermissions or DiscordErrorCode.InsufficientPermissions) {
|
||||||
var newBirthdays = new List<SocketGuildUser>();
|
// Encountered access and/or permission issues despite earlier checks. Quit the loop here.
|
||||||
foreach (var target in names) {
|
|
||||||
var member = g.GetUser(target);
|
|
||||||
if (member == null) continue;
|
|
||||||
if (roleKeeps.Contains(member.Id)) continue; // already has role - do nothing
|
|
||||||
await member.AddRoleAsync(r).ConfigureAwait(false);
|
|
||||||
newBirthdays.Add(member);
|
|
||||||
}
|
}
|
||||||
|
return additions;
|
||||||
return (newBirthdays, (newBirthdays.Count, roleRemoves.Count));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string DefaultAnnounce = "Please wish a happy birthday to %n!";
|
public const string DefaultAnnounce = "Please wish a happy birthday to %n!";
|
||||||
|
@ -142,21 +157,21 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to send an announcement message.
|
/// Attempts to send an announcement message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task AnnounceBirthdaysAsync(
|
internal static async Task AnnounceBirthdaysAsync(GuildConfig settings, SocketGuild g, IEnumerable<SocketGuildUser> names) {
|
||||||
(string?, string?) announce, bool announcePing, SocketTextChannel? c, IEnumerable<SocketGuildUser> names) {
|
var c = g.GetTextChannel((ulong)(settings.ChannelAnnounceId ?? 0));
|
||||||
if (c == null) return;
|
if (c == null) return;
|
||||||
if (!c.Guild.CurrentUser.GetPermissions(c).SendMessages) return;
|
if (!c.Guild.CurrentUser.GetPermissions(c).SendMessages) return;
|
||||||
|
|
||||||
string announceMsg;
|
string announceMsg;
|
||||||
if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce;
|
if (names.Count() == 1) announceMsg = settings.AnnounceMessage ?? settings.AnnounceMessagePl ?? DefaultAnnounce;
|
||||||
else announceMsg = announce.Item2 ?? announce.Item1 ?? DefaultAnnouncePl;
|
else announceMsg = settings.AnnounceMessagePl ?? settings.AnnounceMessage ?? DefaultAnnouncePl;
|
||||||
announceMsg = announceMsg.TrimEnd();
|
announceMsg = announceMsg.TrimEnd();
|
||||||
if (!announceMsg.Contains("%n")) announceMsg += " %n";
|
if (!announceMsg.Contains("%n")) announceMsg += " %n";
|
||||||
|
|
||||||
// Build sorted name list
|
// Build sorted name list
|
||||||
var namestrings = new List<string>();
|
var namestrings = new List<string>();
|
||||||
foreach (var item in names)
|
foreach (var item in names)
|
||||||
namestrings.Add(Common.FormatName(item, announcePing));
|
namestrings.Add(Common.FormatName(item, settings.AnnouncePing));
|
||||||
namestrings.Sort(StringComparer.OrdinalIgnoreCase);
|
namestrings.Sort(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var namedisplay = new StringBuilder();
|
var namedisplay = new StringBuilder();
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
using NpgsqlTypes;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
@ -20,96 +19,49 @@ class DataRetention : BackgroundService {
|
||||||
// On each tick, run only a set group of guilds, each group still processed every ProcessInterval ticks.
|
// On each tick, run only a set group of guilds, each group still processed every ProcessInterval ticks.
|
||||||
if ((tickCount + ShardInstance.ShardId) % ProcessInterval != 0) return;
|
if ((tickCount + ShardInstance.ShardId) % ProcessInterval != 0) return;
|
||||||
|
|
||||||
try {
|
using var db = new BotDatabaseContext();
|
||||||
// A semaphore is used to restrict this work being done concurrently on other shards
|
var now = DateTimeOffset.UtcNow;
|
||||||
// to avoid putting pressure on the SQL connection pool. Clearing old database information
|
int updatedGuilds = 0, updatedUsers = 0;
|
||||||
// ultimately is a low priority among other tasks.
|
|
||||||
await _updateLock.WaitAsync(token).ConfigureAwait(false);
|
|
||||||
} catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) {
|
|
||||||
// Caller does not expect the exception that SemaphoreSlim throws...
|
|
||||||
throw new TaskCanceledException();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Build a list of all values across all guilds to update
|
|
||||||
var updateList = new Dictionary<ulong, List<ulong>>();
|
|
||||||
foreach (var g in ShardInstance.DiscordClient.Guilds) {
|
|
||||||
// Get list of IDs for all users who exist in the database and currently exist in the guild
|
|
||||||
var userList = GuildUserConfiguration.LoadAllAsync(g.Id);
|
|
||||||
var guildUserIds = from gu in g.Users select gu.Id;
|
|
||||||
var savedUserIds = from cu in await userList.ConfigureAwait(false) select cu.UserId;
|
|
||||||
var existingCachedIds = savedUserIds.Intersect(guildUserIds);
|
|
||||||
updateList[g.Id] = existingCachedIds.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||||
|
// Update guild, fetch users from database
|
||||||
|
var dbGuild = db.GuildConfigurations.Where(s => s.GuildId == (long)guild.Id).FirstOrDefault();
|
||||||
|
if (dbGuild == null) continue;
|
||||||
|
dbGuild.LastSeen = now;
|
||||||
|
updatedGuilds++;
|
||||||
|
|
||||||
// Statement for updating last_seen in guilds
|
// Update users
|
||||||
var cUpdateGuild = db.CreateCommand();
|
var localIds = guild.Users.Select(u => (long)u.Id);
|
||||||
cUpdateGuild.CommandText = $"update {GuildConfiguration.BackingTable} set last_seen = now() "
|
var dbSavedIds = db.UserEntries.Where(e => e.GuildId == (long)guild.Id).Select(e => e.UserId);
|
||||||
+ "where guild_id = @Gid";
|
var usersToUpdate = localIds.Intersect(dbSavedIds).ToHashSet();
|
||||||
var pUpdateG = cUpdateGuild.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
foreach (var user in db.UserEntries.Where(e => e.GuildId == (long)guild.Id && usersToUpdate.Contains(e.UserId))) {
|
||||||
cUpdateGuild.Prepare();
|
user.LastSeen = now;
|
||||||
|
updatedUsers++;
|
||||||
// Statement for updating last_seen in guild users
|
|
||||||
var cUpdateGuildUser = db.CreateCommand();
|
|
||||||
cUpdateGuildUser.CommandText = $"update {GuildUserConfiguration.BackingTable} set last_seen = now() "
|
|
||||||
+ "where guild_id = @Gid and user_id = @Uid";
|
|
||||||
var pUpdateGU_g = cUpdateGuildUser.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
|
||||||
var pUpdateGU_u = cUpdateGuildUser.Parameters.Add("@Uid", NpgsqlDbType.Bigint);
|
|
||||||
cUpdateGuildUser.Prepare();
|
|
||||||
|
|
||||||
// Do actual updates
|
|
||||||
int updatedGuilds = 0;
|
|
||||||
int updatedUsers = 0;
|
|
||||||
using (var tUpdate = db.BeginTransaction()) {
|
|
||||||
foreach (var item in updateList) {
|
|
||||||
var guild = item.Key;
|
|
||||||
var userlist = item.Value;
|
|
||||||
|
|
||||||
pUpdateG.Value = (long)guild;
|
|
||||||
updatedGuilds += await cUpdateGuild.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
pUpdateGU_g.Value = (long)guild;
|
|
||||||
foreach (var userid in userlist) {
|
|
||||||
pUpdateGU_u.Value = (long)userid;
|
|
||||||
updatedUsers += await cUpdateGuildUser.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await tUpdate.CommitAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
// And let go of old data
|
||||||
|
var staleGuilds = db.GuildConfigurations.Where(s => now - TimeSpan.FromDays(StaleGuildThreshold) > s.LastSeen);
|
||||||
|
var staleUsers = db.UserEntries.Where(e => now - TimeSpan.FromDays(StaleUserThreashold) > e.LastSeen);
|
||||||
|
int staleGuildCount = staleGuilds.Count(), staleUserCount = staleUsers.Count();
|
||||||
|
db.GuildConfigurations.RemoveRange(staleGuilds);
|
||||||
|
db.UserEntries.RemoveRange(staleUsers);
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
|
|
||||||
var resultText = new StringBuilder();
|
var resultText = new StringBuilder();
|
||||||
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");
|
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");
|
||||||
|
if (staleGuildCount != 0 || staleUserCount != 0) {
|
||||||
// Deletes both guild and user data if it hasn't been seen for over the threshold defined at the top of this file
|
|
||||||
// Expects referencing tables to have 'on delete cascade'
|
|
||||||
int staleGuilds, staleUsers;
|
|
||||||
using (var tRemove = db.BeginTransaction()) {
|
|
||||||
using (var c = db.CreateCommand()) {
|
|
||||||
c.CommandText = $"delete from {GuildConfiguration.BackingTable}" +
|
|
||||||
$" where (now() - interval '{StaleGuildThreshold} days') > last_seen";
|
|
||||||
staleGuilds = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
using (var c = db.CreateCommand()) {
|
|
||||||
c.CommandText = $"delete from {GuildUserConfiguration.BackingTable}" +
|
|
||||||
$" where (now() - interval '{StaleUserThreashold} days') > last_seen";
|
|
||||||
staleUsers = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
await tRemove.CommitAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
if (staleGuilds != 0 || staleUsers != 0) {
|
|
||||||
resultText.Append(" Discarded ");
|
resultText.Append(" Discarded ");
|
||||||
if (staleGuilds != 0) {
|
if (staleGuildCount != 0) {
|
||||||
resultText.Append($"{staleGuilds} guilds");
|
resultText.Append($"{staleGuildCount} guilds");
|
||||||
if (staleUsers != 0) resultText.Append(", ");
|
if (staleUserCount != 0) resultText.Append(", ");
|
||||||
}
|
}
|
||||||
if (staleUsers != 0) {
|
if (staleUserCount != 0) {
|
||||||
resultText.Append($"{staleUsers} users");
|
resultText.Append($"{staleUserCount} users");
|
||||||
}
|
}
|
||||||
resultText.Append('.');
|
resultText.Append('.');
|
||||||
}
|
}
|
||||||
Log(resultText.ToString());
|
Log(resultText.ToString());
|
||||||
} finally {
|
|
||||||
_updateLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
using System;
|
using System.Text;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
|
@ -11,7 +7,7 @@ namespace BirthdayBot.BackgroundServices;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class ExternalStatisticsReporting : BackgroundService {
|
class ExternalStatisticsReporting : BackgroundService {
|
||||||
const int ProcessInterval = 1200 / ShardBackgroundWorker.Interval; // Process every ~20 minutes
|
const int ProcessInterval = 1200 / ShardBackgroundWorker.Interval; // Process every ~20 minutes
|
||||||
const int ProcessOffset = 300 / ShardBackgroundWorker.Interval; // Begin processing 5 minutes after shard start
|
const int ProcessOffset = 300 / ShardBackgroundWorker.Interval; // Begin processing ~5 minutes after shard start
|
||||||
|
|
||||||
private static readonly HttpClient _httpClient = new();
|
private static readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ class ShardBackgroundWorker : IDisposable {
|
||||||
_tickCount++;
|
_tickCount++;
|
||||||
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
|
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
|
||||||
} catch (Exception ex) when (ex is not TaskCanceledException) {
|
} catch (Exception ex) when (ex is not TaskCanceledException) {
|
||||||
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString());
|
Instance.Log(CurrentExecutingService, ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CurrentExecutingService = null;
|
CurrentExecutingService = null;
|
||||||
|
|
|
@ -23,10 +23,17 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.4.1" />
|
<PackageReference Include="Discord.Net" Version="3.4.1" />
|
||||||
|
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="NodaTime" Version="3.0.9" />
|
<PackageReference Include="NodaTime" Version="3.0.9" />
|
||||||
<PackageReference Include="Npgsql" Version="6.0.3" />
|
<PackageReference Include="Npgsql" Version="6.0.3" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -27,6 +27,8 @@ class Configuration {
|
||||||
public int ShardAmount { get; }
|
public int ShardAmount { get; }
|
||||||
public int ShardTotal { get; }
|
public int ShardTotal { get; }
|
||||||
|
|
||||||
|
public string DatabaseConnectionString { get; }
|
||||||
|
|
||||||
public Configuration(string[] args) {
|
public Configuration(string[] args) {
|
||||||
var cmdline = CmdLineOpts.Parse(args);
|
var cmdline = CmdLineOpts.Parse(args);
|
||||||
|
|
||||||
|
@ -74,7 +76,7 @@ class Configuration {
|
||||||
};
|
};
|
||||||
var sqldb = ReadConfKey<string>(jc, KeySqlDatabase, false);
|
var sqldb = ReadConfKey<string>(jc, KeySqlDatabase, false);
|
||||||
if (sqldb != null) csb.Database = sqldb; // Optional database setting
|
if (sqldb != null) csb.Database = sqldb; // Optional database setting
|
||||||
Database.DBConnectionString = csb.ToString();
|
DatabaseConnectionString = csb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
||||||
|
|
18
Data/BlocklistEntry.cs
Normal file
18
Data/BlocklistEntry.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data;
|
||||||
|
|
||||||
|
[Table("banned_users")]
|
||||||
|
public class BlocklistEntry {
|
||||||
|
[Key]
|
||||||
|
[Column("guild_id")]
|
||||||
|
public long GuildId { get; set; }
|
||||||
|
[Key]
|
||||||
|
[Column("user_id")]
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(GuildConfig.GuildId))]
|
||||||
|
[InverseProperty(nameof(GuildConfig.BlockedUsers))]
|
||||||
|
public GuildConfig Guild { get; set; } = null!;
|
||||||
|
}
|
66
Data/BotDatabaseContext.cs
Normal file
66
Data/BotDatabaseContext.cs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data;
|
||||||
|
|
||||||
|
public class BotDatabaseContext : DbContext {
|
||||||
|
private static string? _npgsqlConnectionString;
|
||||||
|
internal static string NpgsqlConnectionString {
|
||||||
|
#if DEBUG
|
||||||
|
get {
|
||||||
|
if (_npgsqlConnectionString != null) return _npgsqlConnectionString;
|
||||||
|
Program.Log(nameof(BotDatabaseContext), "Using hardcoded connection string!");
|
||||||
|
return _npgsqlConnectionString ?? "Host=localhost;Username=birthdaybot;Password=bb";
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
get => _npgsqlConnectionString!;
|
||||||
|
#endif
|
||||||
|
set => _npgsqlConnectionString ??= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<BlocklistEntry> BlocklistEntries { get; set; } = null!;
|
||||||
|
public DbSet<GuildConfig> GuildConfigurations { get; set; } = null!;
|
||||||
|
public DbSet<UserEntry> UserEntries { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
=> optionsBuilder
|
||||||
|
.UseNpgsql(NpgsqlConnectionString)
|
||||||
|
#if DEBUG
|
||||||
|
.LogTo((string line) => Program.Log("EF", line), Microsoft.Extensions.Logging.LogLevel.Information)
|
||||||
|
#endif
|
||||||
|
.UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
|
modelBuilder.Entity<BlocklistEntry>(entity => {
|
||||||
|
entity.HasKey(e => new { e.GuildId, e.UserId })
|
||||||
|
.HasName("banned_users_pkey");
|
||||||
|
|
||||||
|
entity.HasOne(d => d.Guild)
|
||||||
|
.WithMany(p => p.BlockedUsers)
|
||||||
|
.HasForeignKey(d => d.GuildId)
|
||||||
|
.HasConstraintName("banned_users_guild_id_fkey")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<GuildConfig>(entity => {
|
||||||
|
entity.HasKey(e => e.GuildId)
|
||||||
|
.HasName("settings_pkey");
|
||||||
|
|
||||||
|
entity.Property(e => e.GuildId).ValueGeneratedNever();
|
||||||
|
|
||||||
|
entity.Property(e => e.LastSeen).HasDefaultValueSql("now()");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserEntry>(entity => {
|
||||||
|
entity.HasKey(e => new { e.GuildId, e.UserId })
|
||||||
|
.HasName("user_birthdays_pkey");
|
||||||
|
|
||||||
|
entity.Property(e => e.LastSeen).HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
entity.HasOne(d => d.Guild)
|
||||||
|
.WithMany(p => p.UserEntries)
|
||||||
|
.HasForeignKey(d => d.GuildId)
|
||||||
|
.HasConstraintName("user_birthdays_guild_id_fkey")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.Data;
|
namespace BirthdayBot.Data;
|
||||||
|
|
||||||
|
[Obsolete(ObsoleteReason, error: false)]
|
||||||
internal static class Database {
|
internal static class Database {
|
||||||
public static string DBConnectionString { get; set; }
|
public const string ObsoleteReason = "Will be removed in favor of EF6 stuff when text commands are removed";
|
||||||
|
|
||||||
|
public static string DBConnectionString { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets up and opens a database connection.
|
/// Sets up and opens a database connection.
|
||||||
|
|
|
@ -2,20 +2,18 @@
|
||||||
|
|
||||||
internal static class Extensions {
|
internal static class Extensions {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the database-backed bot configuration for this guild.
|
/// Gets the corresponding <see cref="GuildConfig"/> for this guild, or a new one if one does not exist.
|
||||||
|
/// If it doesn't exist in the database, <see cref="GuildConfig.IsNew"/> returns true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static async Task<GuildConfiguration> GetConfigAsync(this SocketGuild guild)
|
public static GuildConfig GetConfigOrNew(this SocketGuild guild, BotDatabaseContext db)
|
||||||
=> await GuildConfiguration.LoadAsync(guild.Id, false);
|
=> db.GuildConfigurations.Where(g => g.GuildId == (long)guild.Id).FirstOrDefault()
|
||||||
|
?? new GuildConfig() { IsNew = true, GuildId = (long)guild.Id };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a collection of all existing user configurations for this guild.
|
/// Gets the corresponding <see cref="UserEntry"/> for this user in this guild, or a new one if one does not exist.
|
||||||
|
/// If it doesn't exist in the database, <see cref="UserEntry.IsNew"/> returns true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static async Task<IEnumerable<GuildUserConfiguration>> GetUserConfigurationsAsync(this SocketGuild guild)
|
public static UserEntry GetUserEntryOrNew(this SocketGuildUser user, BotDatabaseContext db)
|
||||||
=> await GuildUserConfiguration.LoadAllAsync(guild.Id);
|
=> db.UserEntries.Where(u => u.GuildId == (long)user.Guild.Id && u.UserId == (long)user.Id).FirstOrDefault()
|
||||||
|
?? new UserEntry() { IsNew = true, GuildId = (long)user.Guild.Id, UserId = (long)user.Id };
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the database-backed bot configuration (birthday info) for this guild user.
|
|
||||||
/// </summary>
|
|
||||||
internal static async Task<GuildUserConfiguration> GetConfigAsync(this SocketGuildUser user)
|
|
||||||
=> await GuildUserConfiguration.LoadAsync(user.Guild.Id, user.Id);
|
|
||||||
}
|
}
|
45
Data/GuildConfig.cs
Normal file
45
Data/GuildConfig.cs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data;
|
||||||
|
|
||||||
|
[Table("settings")]
|
||||||
|
public class GuildConfig {
|
||||||
|
public GuildConfig() {
|
||||||
|
BlockedUsers = new HashSet<BlocklistEntry>();
|
||||||
|
UserEntries = new HashSet<UserEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key]
|
||||||
|
[Column("guild_id")]
|
||||||
|
public long GuildId { get; set; }
|
||||||
|
[Column("role_id")]
|
||||||
|
public long? RoleId { get; set; }
|
||||||
|
[Column("channel_announce_id")]
|
||||||
|
public long? ChannelAnnounceId { get; set; }
|
||||||
|
[Column("time_zone")]
|
||||||
|
public string? TimeZone { get; set; }
|
||||||
|
[Column("moderated")]
|
||||||
|
public bool Moderated { get; set; }
|
||||||
|
[Column("moderator_role")]
|
||||||
|
public long? ModeratorRole { get; set; }
|
||||||
|
[Column("announce_message")]
|
||||||
|
public string? AnnounceMessage { get; set; }
|
||||||
|
[Column("announce_message_pl")]
|
||||||
|
public string? AnnounceMessagePl { get; set; }
|
||||||
|
[Column("announce_ping")]
|
||||||
|
public bool AnnouncePing { get; set; }
|
||||||
|
[Column("last_seen")]
|
||||||
|
public DateTimeOffset LastSeen { get; set; }
|
||||||
|
|
||||||
|
[InverseProperty(nameof(BlocklistEntry.Guild))]
|
||||||
|
public ICollection<BlocklistEntry> BlockedUsers { get; set; }
|
||||||
|
[InverseProperty(nameof(UserEntry.Guild))]
|
||||||
|
public ICollection<UserEntry> UserEntries { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets if this instance is new and does not (yet) exist in the database.
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public bool IsNew { get; set; }
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ namespace BirthdayBot.Data;
|
||||||
/// Represents guild-specific configuration as exists in the database.
|
/// Represents guild-specific configuration as exists in the database.
|
||||||
/// Updating any property requires a call to <see cref="UpdateAsync"/> for changes to take effect.
|
/// Updating any property requires a call to <see cref="UpdateAsync"/> for changes to take effect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete(Database.ObsoleteReason, error: false)]
|
||||||
class GuildConfiguration {
|
class GuildConfiguration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets this configuration's corresponding guild ID.
|
/// Gets this configuration's corresponding guild ID.
|
||||||
|
@ -73,7 +74,6 @@ class GuildConfiguration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if the specified user is blocked by current guild policy (block list or moderated mode).
|
/// Checks if the specified user is blocked by current guild policy (block list or moderated mode).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Block lists should be reimplemented in a more resource-efficient manner later.", false)]
|
|
||||||
public async Task<bool> IsUserBlockedAsync(ulong userId) {
|
public async Task<bool> IsUserBlockedAsync(ulong userId) {
|
||||||
if (IsModerated) return true;
|
if (IsModerated) return true;
|
||||||
return await IsUserInBlocklistAsync(userId).ConfigureAwait(false);
|
return await IsUserInBlocklistAsync(userId).ConfigureAwait(false);
|
||||||
|
@ -82,7 +82,6 @@ class GuildConfiguration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if the given user exists in the block list.
|
/// Checks if the given user exists in the block list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Block lists should be reimplemented in a more resource-efficient manner later.", false)]
|
|
||||||
public async Task<bool> IsUserInBlocklistAsync(ulong userId) {
|
public async Task<bool> IsUserInBlocklistAsync(ulong userId) {
|
||||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||||
using var c = db.CreateCommand();
|
using var c = db.CreateCommand();
|
||||||
|
@ -99,7 +98,6 @@ class GuildConfiguration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the specified user to the block list corresponding to this guild.
|
/// Adds the specified user to the block list corresponding to this guild.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Block lists will be reimplemented in a more practical manner later.", false)]
|
|
||||||
public async Task BlockUserAsync(ulong userId) {
|
public async Task BlockUserAsync(ulong userId) {
|
||||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||||
using var c = db.CreateCommand();
|
using var c = db.CreateCommand();
|
||||||
|
@ -117,7 +115,6 @@ class GuildConfiguration {
|
||||||
/// Removes the specified user from the block list corresponding to this guild.
|
/// Removes the specified user from the block list corresponding to this guild.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>True if a user has been removed, false if the requested user was not in this list.</returns>
|
/// <returns>True if a user has been removed, false if the requested user was not in this list.</returns>
|
||||||
[Obsolete("Block lists will be reimplemented in a more practical manner later.", false)]
|
|
||||||
public async Task<bool> UnblockUserAsync(ulong userId) {
|
public async Task<bool> UnblockUserAsync(ulong userId) {
|
||||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||||
using var c = db.CreateCommand();
|
using var c = db.CreateCommand();
|
||||||
|
@ -134,7 +131,6 @@ class GuildConfiguration {
|
||||||
/// Checks if the given user can be considered a bot moderator.
|
/// 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.
|
/// Checks for either the Manage Guild permission or if the user is within a predetermined role.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Usage should be phased out when text commands are removed. Use PreconditionAttribute from now on.", error: false)]
|
|
||||||
public bool IsBotModerator(SocketGuildUser user)
|
public bool IsBotModerator(SocketGuildUser user)
|
||||||
=> user.GuildPermissions.ManageGuild || (ModeratorRole.HasValue && user.Roles.Any(r => r.Id == ModeratorRole.Value));
|
=> user.GuildPermissions.ManageGuild || (ModeratorRole.HasValue && user.Roles.Any(r => r.Id == ModeratorRole.Value));
|
||||||
|
|
||||||
|
@ -142,6 +138,7 @@ class GuildConfiguration {
|
||||||
public const string BackingTable = "settings";
|
public const string BackingTable = "settings";
|
||||||
public const string BackingTableBans = "banned_users";
|
public const string BackingTableBans = "banned_users";
|
||||||
|
|
||||||
|
[Obsolete("DELETE THIS", error: true)]
|
||||||
internal static async Task DatabaseSetupAsync(NpgsqlConnection db) {
|
internal static async Task DatabaseSetupAsync(NpgsqlConnection db) {
|
||||||
using (var c = db.CreateCommand()) {
|
using (var c = db.CreateCommand()) {
|
||||||
c.CommandText = $"create table if not exists {BackingTable} ("
|
c.CommandText = $"create table if not exists {BackingTable} ("
|
||||||
|
@ -175,7 +172,6 @@ class GuildConfiguration {
|
||||||
/// If true, this method shall not create a new entry and will return null if the guild does
|
/// If true, this method shall not create a new entry and will return null if the guild does
|
||||||
/// not exist in the database.
|
/// not exist in the database.
|
||||||
/// </param>
|
/// </param>
|
||||||
[Obsolete("Begin using extension method to retrieve necessary data instead.", false)]
|
|
||||||
public static async Task<GuildConfiguration?> LoadAsync(ulong guildId, bool nullIfUnknown) {
|
public static async Task<GuildConfiguration?> LoadAsync(ulong guildId, bool nullIfUnknown) {
|
||||||
// TODO nullable static analysis problem: how to indicate non-null return when nullIfUnknown parameter is true?
|
// TODO nullable static analysis problem: how to indicate non-null return when nullIfUnknown parameter is true?
|
||||||
using (var db = await Database.OpenConnectionAsync().ConfigureAwait(false)) {
|
using (var db = await Database.OpenConnectionAsync().ConfigureAwait(false)) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ namespace BirthdayBot.Data;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents configuration for a guild user as may exist in the database.
|
/// Represents configuration for a guild user as may exist in the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete(Database.ObsoleteReason, error: false)]
|
||||||
class GuildUserConfiguration {
|
class GuildUserConfiguration {
|
||||||
public ulong GuildId { get; }
|
public ulong GuildId { get; }
|
||||||
public ulong UserId { get; }
|
public ulong UserId { get; }
|
||||||
|
@ -106,7 +107,6 @@ class GuildUserConfiguration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to retrieve a user's configuration. Returns a new, updateable instance if none is found.
|
/// Attempts to retrieve a user's configuration. Returns a new, updateable instance if none is found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Migrate to using extension methods to retrieve necessary data instead.", false)]
|
|
||||||
public static async Task<GuildUserConfiguration> LoadAsync(ulong guildId, ulong userId) {
|
public static async Task<GuildUserConfiguration> LoadAsync(ulong guildId, ulong userId) {
|
||||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||||
using var c = db.CreateCommand();
|
using var c = db.CreateCommand();
|
||||||
|
@ -123,7 +123,6 @@ class GuildUserConfiguration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all known user configuration records associated with the specified guild.
|
/// Gets all known user configuration records associated with the specified guild.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Migrate to using extension methods to retrieve necessary data instead.", false)]
|
|
||||||
public static async Task<IEnumerable<GuildUserConfiguration>> LoadAllAsync(ulong guildId) {
|
public static async Task<IEnumerable<GuildUserConfiguration>> LoadAllAsync(ulong guildId) {
|
||||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||||
using var c = db.CreateCommand();
|
using var c = db.CreateCommand();
|
||||||
|
|
160
Data/Migrations/20220319195852_InitialEFSetup.Designer.cs
generated
Normal file
160
Data/Migrations/20220319195852_InitialEFSetup.Designer.cs
generated
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BirthdayBot.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(BotDatabaseContext))]
|
||||||
|
[Migration("20220319195852_InitialEFSetup")]
|
||||||
|
partial class InitialEFSetup
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.BlocklistEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("GuildId", "UserId")
|
||||||
|
.HasName("banned_users_pkey");
|
||||||
|
|
||||||
|
b.ToTable("banned_users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.GuildConfig", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<string>("AnnounceMessage")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("announce_message");
|
||||||
|
|
||||||
|
b.Property<string>("AnnounceMessagePl")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("announce_message_pl");
|
||||||
|
|
||||||
|
b.Property<bool>("AnnouncePing")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("announce_ping");
|
||||||
|
|
||||||
|
b.Property<long?>("ChannelAnnounceId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("channel_announce_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastSeen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_seen")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<bool>("Moderated")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("moderated");
|
||||||
|
|
||||||
|
b.Property<long?>("ModeratorRole")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("moderator_role");
|
||||||
|
|
||||||
|
b.Property<long?>("RoleId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_id");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("time_zone");
|
||||||
|
|
||||||
|
b.HasKey("GuildId")
|
||||||
|
.HasName("settings_pkey");
|
||||||
|
|
||||||
|
b.ToTable("settings", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.UserEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<int>("BirthDay")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("birth_day");
|
||||||
|
|
||||||
|
b.Property<int>("BirthMonth")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("birth_month");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastSeen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_seen")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("time_zone");
|
||||||
|
|
||||||
|
b.HasKey("GuildId", "UserId")
|
||||||
|
.HasName("user_birthdays_pkey");
|
||||||
|
|
||||||
|
b.ToTable("user_birthdays", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.BlocklistEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthdayBot.Data.GuildConfig", "Guild")
|
||||||
|
.WithMany("BlockedUsers")
|
||||||
|
.HasForeignKey("GuildId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("banned_users_guild_id_fkey");
|
||||||
|
|
||||||
|
b.Navigation("Guild");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.UserEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthdayBot.Data.GuildConfig", "Guild")
|
||||||
|
.WithMany("UserEntries")
|
||||||
|
.HasForeignKey("GuildId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("user_birthdays_guild_id_fkey");
|
||||||
|
|
||||||
|
b.Navigation("Guild");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.GuildConfig", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BlockedUsers");
|
||||||
|
|
||||||
|
b.Navigation("UserEntries");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
Data/Migrations/20220319195852_InitialEFSetup.cs
Normal file
89
Data/Migrations/20220319195852_InitialEFSetup.cs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
// command used:
|
||||||
|
// dotnet ef migrations add InitialEFSetup --output-dir Data/Migrations
|
||||||
|
// (don't forget to replace with a proper migration name)
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class InitialEFSetup : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guild_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
role_id = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
channel_announce_id = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
time_zone = table.Column<string>(type: "text", nullable: true),
|
||||||
|
moderated = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
moderator_role = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
announce_message = table.Column<string>(type: "text", nullable: true),
|
||||||
|
announce_message_pl = table.Column<string>(type: "text", nullable: true),
|
||||||
|
announce_ping = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
last_seen = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("settings_pkey", x => x.guild_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "banned_users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guild_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
user_id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("banned_users_pkey", x => new { x.guild_id, x.user_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "banned_users_guild_id_fkey",
|
||||||
|
column: x => x.guild_id,
|
||||||
|
principalTable: "settings",
|
||||||
|
principalColumn: "guild_id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_birthdays",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guild_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
birth_month = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
birth_day = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
time_zone = table.Column<string>(type: "text", nullable: true),
|
||||||
|
last_seen = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("user_birthdays_pkey", x => new { x.guild_id, x.user_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "user_birthdays_guild_id_fkey",
|
||||||
|
column: x => x.guild_id,
|
||||||
|
principalTable: "settings",
|
||||||
|
principalColumn: "guild_id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "banned_users");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_birthdays");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
158
Data/Migrations/BotDatabaseContextModelSnapshot.cs
Normal file
158
Data/Migrations/BotDatabaseContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BirthdayBot.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(BotDatabaseContext))]
|
||||||
|
partial class BotDatabaseContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.BlocklistEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("GuildId", "UserId")
|
||||||
|
.HasName("banned_users_pkey");
|
||||||
|
|
||||||
|
b.ToTable("banned_users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.GuildConfig", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<string>("AnnounceMessage")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("announce_message");
|
||||||
|
|
||||||
|
b.Property<string>("AnnounceMessagePl")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("announce_message_pl");
|
||||||
|
|
||||||
|
b.Property<bool>("AnnouncePing")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("announce_ping");
|
||||||
|
|
||||||
|
b.Property<long?>("ChannelAnnounceId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("channel_announce_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastSeen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_seen")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<bool>("Moderated")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("moderated");
|
||||||
|
|
||||||
|
b.Property<long?>("ModeratorRole")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("moderator_role");
|
||||||
|
|
||||||
|
b.Property<long?>("RoleId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_id");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("time_zone");
|
||||||
|
|
||||||
|
b.HasKey("GuildId")
|
||||||
|
.HasName("settings_pkey");
|
||||||
|
|
||||||
|
b.ToTable("settings", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.UserEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<int>("BirthDay")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("birth_day");
|
||||||
|
|
||||||
|
b.Property<int>("BirthMonth")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("birth_month");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastSeen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_seen")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("time_zone");
|
||||||
|
|
||||||
|
b.HasKey("GuildId", "UserId")
|
||||||
|
.HasName("user_birthdays_pkey");
|
||||||
|
|
||||||
|
b.ToTable("user_birthdays", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.BlocklistEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthdayBot.Data.GuildConfig", "Guild")
|
||||||
|
.WithMany("BlockedUsers")
|
||||||
|
.HasForeignKey("GuildId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("banned_users_guild_id_fkey");
|
||||||
|
|
||||||
|
b.Navigation("Guild");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.UserEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthdayBot.Data.GuildConfig", "Guild")
|
||||||
|
.WithMany("UserEntries")
|
||||||
|
.HasForeignKey("GuildId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("user_birthdays_guild_id_fkey");
|
||||||
|
|
||||||
|
b.Navigation("Guild");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthdayBot.Data.GuildConfig", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BlockedUsers");
|
||||||
|
|
||||||
|
b.Navigation("UserEntries");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Data/UserEntry.cs
Normal file
32
Data/UserEntry.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace BirthdayBot.Data;
|
||||||
|
|
||||||
|
[Table("user_birthdays")]
|
||||||
|
public class UserEntry {
|
||||||
|
[Key]
|
||||||
|
[Column("guild_id")]
|
||||||
|
public long GuildId { get; set; }
|
||||||
|
[Key]
|
||||||
|
[Column("user_id")]
|
||||||
|
public long UserId { get; set; }
|
||||||
|
[Column("birth_month")]
|
||||||
|
public int BirthMonth { get; set; }
|
||||||
|
[Column("birth_day")]
|
||||||
|
public int BirthDay { get; set; }
|
||||||
|
[Column("time_zone")]
|
||||||
|
public string? TimeZone { get; set; }
|
||||||
|
[Column("last_seen")]
|
||||||
|
public DateTimeOffset LastSeen { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(GuildConfig.GuildId))]
|
||||||
|
[InverseProperty(nameof(GuildConfig.UserEntries))]
|
||||||
|
public GuildConfig Guild { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets if this instance is new and does not (yet) exist in the database.
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public bool IsNew { get; set; }
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot;
|
namespace BirthdayBot;
|
||||||
|
|
||||||
|
@ -22,6 +20,9 @@ class Program {
|
||||||
Environment.Exit((int)ExitCodes.ConfigError);
|
Environment.Exit((int)ExitCodes.ConfigError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BotDatabaseContext.NpgsqlConnectionString = cfg.DatabaseConnectionString;
|
||||||
|
|
||||||
|
Database.DBConnectionString = cfg.DatabaseConnectionString;
|
||||||
try {
|
try {
|
||||||
await Database.DoInitialDatabaseSetupAsync();
|
await Database.DoInitialDatabaseSetupAsync();
|
||||||
} catch (Npgsql.NpgsqlException e) {
|
} catch (Npgsql.NpgsqlException e) {
|
||||||
|
|
|
@ -127,6 +127,7 @@ public sealed class ShardInstance : IDisposable {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CS0618
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
|
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -166,6 +167,7 @@ public sealed class ShardInstance : IDisposable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
// Slash command preparation and invocation
|
// Slash command preparation and invocation
|
||||||
private async Task DiscordClient_InteractionCreated(SocketInteraction arg) {
|
private async Task DiscordClient_InteractionCreated(SocketInteraction arg) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using BirthdayBot.Data;
|
#pragma warning disable CS0618
|
||||||
|
using BirthdayBot.Data;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using BirthdayBot.Data;
|
#pragma warning disable CS0618
|
||||||
|
using BirthdayBot.Data;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthdayBot.TextCommands;
|
namespace BirthdayBot.TextCommands;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using BirthdayBot.Data;
|
#pragma warning disable CS0618
|
||||||
|
using BirthdayBot.Data;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthdayBot.TextCommands;
|
namespace BirthdayBot.TextCommands;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using BirthdayBot.Data;
|
#pragma warning disable CS0618
|
||||||
|
using BirthdayBot.Data;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using BirthdayBot.Data;
|
#pragma warning disable CS0618
|
||||||
|
using BirthdayBot.Data;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace BirthdayBot.TextCommands;
|
namespace BirthdayBot.TextCommands;
|
||||||
|
|
Loading…
Reference in a new issue