mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-22 05: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);
|
||||
await user.UpdateAsync(inmonth, inday, inzone ?? user.TimeZone).ConfigureAwait(false);
|
||||
using var db = new BotDatabaseContext();
|
||||
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)}**" +
|
||||
(inzone == null ? "" : $", with time zone {inzone}") + ".").ConfigureAwait(false);
|
||||
|
@ -50,31 +55,35 @@ public class BirthdayModule : BotModuleBase {
|
|||
|
||||
[SlashCommand("timezone", HelpCmdSetZone)]
|
||||
public async Task CmdSetZone([Summary(description: HelpOptZone)] string zone) {
|
||||
var user = await ((SocketGuildUser)Context.User).GetConfigAsync().ConfigureAwait(false);
|
||||
if (!user.IsKnown) {
|
||||
using var db = new BotDatabaseContext();
|
||||
|
||||
var user = ((SocketGuildUser)Context.User).GetUserEntryOrNew(db);
|
||||
if (user.IsNew) {
|
||||
await RespondAsync(":x: You do not have a birthday set.", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
string inzone;
|
||||
string newzone;
|
||||
try {
|
||||
inzone = ParseTimeZone(zone);
|
||||
newzone = ParseTimeZone(zone);
|
||||
} catch (FormatException e) {
|
||||
await RespondAsync(e.Message, ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
await user.UpdateAsync(user.BirthMonth, user.BirthDay, inzone).ConfigureAwait(false);
|
||||
await RespondAsync($":white_check_mark: Your time zone has been set to **{inzone}**.").ConfigureAwait(false);
|
||||
user.TimeZone = newzone;
|
||||
await db.SaveChangesAsync();
|
||||
await RespondAsync($":white_check_mark: Your time zone has been set to **{newzone}**.").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[SlashCommand("remove", HelpCmdRemove)]
|
||||
public async Task CmdRemove() {
|
||||
var user = await ((SocketGuildUser)Context.User).GetConfigAsync().ConfigureAwait(false);
|
||||
if (user.IsKnown) {
|
||||
await user.DeleteAsync().ConfigureAwait(false);
|
||||
await RespondAsync(":white_check_mark: Your birthday in this server has been removed.")
|
||||
.ConfigureAwait(false);
|
||||
using var db = new BotDatabaseContext();
|
||||
var user = ((SocketGuildUser)Context.User).GetUserEntryOrNew(db);
|
||||
if (!user.IsNew) {
|
||||
db.UserEntries.Remove(user);
|
||||
await db.SaveChangesAsync();
|
||||
await RespondAsync(":white_check_mark: Your birthday in this server has been removed.");
|
||||
} else {
|
||||
await RespondAsync(":white_check_mark: Your birthday is not registered.")
|
||||
.ConfigureAwait(false);
|
||||
|
@ -83,12 +92,15 @@ public class BirthdayModule : BotModuleBase {
|
|||
|
||||
[SlashCommand("get", "Gets a user's birthday.")]
|
||||
public async Task CmdGetBday([Summary(description: "Optional: The user's birthday to look up.")] SocketGuildUser? user = null) {
|
||||
var self = user is null;
|
||||
if (self) user = (SocketGuildUser)Context.User;
|
||||
var targetdata = await user!.GetConfigAsync().ConfigureAwait(false);
|
||||
using var db = new BotDatabaseContext();
|
||||
|
||||
if (!targetdata.IsKnown) {
|
||||
if (self) await RespondAsync(":x: You do not have your birthday registered.", ephemeral: true).ConfigureAwait(false);
|
||||
var isSelf = user is null;
|
||||
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);
|
||||
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
|
||||
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
|
||||
bool hasOutputOneLine = false;
|
||||
|
@ -182,7 +194,7 @@ public class BirthdayModule : BotModuleBase {
|
|||
return;
|
||||
}
|
||||
|
||||
var bdlist = await GetSortedUsersAsync(Context.Guild).ConfigureAwait(false);
|
||||
var bdlist = GetSortedUserList(Context.Guild);
|
||||
|
||||
var filename = "birthdaybot-" + Context.Guild.Id;
|
||||
Stream fileoutput;
|
||||
|
@ -201,27 +213,26 @@ public class BirthdayModule : BotModuleBase {
|
|||
/// Fetches all guild birthdays and places them into an easily usable structure.
|
||||
/// Users currently not in the guild are not included in the result.
|
||||
/// </summary>
|
||||
private static async Task<List<ListItem>> GetSortedUsersAsync(SocketGuild guild) {
|
||||
using var db = await Database.OpenConnectionAsync();
|
||||
using var c = db.CreateCommand();
|
||||
c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserConfiguration.BackingTable
|
||||
+ " where guild_id = @Gid order by birth_month, birth_day";
|
||||
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
||||
c.Prepare();
|
||||
using var r = await c.ExecuteReaderAsync();
|
||||
var result = new List<ListItem>();
|
||||
while (await r.ReadAsync()) {
|
||||
var id = (ulong)r.GetInt64(0);
|
||||
var month = r.GetInt32(1);
|
||||
var day = r.GetInt32(2);
|
||||
private static List<ListItem> GetSortedUserList(SocketGuild guild) {
|
||||
using var db = new BotDatabaseContext();
|
||||
var query = from row in db.UserEntries
|
||||
where row.GuildId == (long)guild.Id
|
||||
orderby row.BirthMonth, row.BirthDay
|
||||
select new {
|
||||
UserId = (ulong)row.UserId,
|
||||
Month = row.BirthMonth,
|
||||
Day = row.BirthDay
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
result.Add(new ListItem() {
|
||||
BirthMonth = month,
|
||||
BirthDay = day,
|
||||
DateIndex = DateIndex(month, day),
|
||||
BirthMonth = row.Month,
|
||||
BirthDay = row.Day,
|
||||
DateIndex = DateIndex(row.Month, row.Day),
|
||||
UserId = guildUser.Id,
|
||||
DisplayName = Common.FormatName(guildUser, false)
|
||||
});
|
||||
|
|
|
@ -24,8 +24,12 @@ public class BirthdayOverrideModule : BotModuleBase {
|
|||
return;
|
||||
}
|
||||
|
||||
var user = await target.GetConfigAsync().ConfigureAwait(false);
|
||||
await user.UpdateAsync(inmonth, inday, user.TimeZone).ConfigureAwait(false);
|
||||
using var db = new BotDatabaseContext();
|
||||
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 " +
|
||||
$"**{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.")]
|
||||
public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)]SocketGuildUser target,
|
||||
[Summary(description: HelpOptZone)]string zone) {
|
||||
var user = await target.GetConfigAsync().ConfigureAwait(false);
|
||||
if (!user.IsKnown) {
|
||||
using var db = new BotDatabaseContext();
|
||||
|
||||
var user = target.GetUserEntryOrNew(db);
|
||||
if (user.IsNew) {
|
||||
await RespondAsync($":x: {Common.FormatName(target, false)} does not have a birthday set.")
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
string inzone;
|
||||
string newzone;
|
||||
try {
|
||||
inzone = ParseTimeZone(zone);
|
||||
newzone = ParseTimeZone(zone);
|
||||
} catch (FormatException e) {
|
||||
await RespondAsync(e.Message, ephemeral: true).ConfigureAwait(false);
|
||||
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 " +
|
||||
$"**{inzone}**.").ConfigureAwait(false);
|
||||
$"**{newzone}**.").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[SlashCommand("remove-birthday", HelpPfxModOnly + "Remove a user's birthday information on their behalf.")]
|
||||
public async Task OvRemove([Summary(description: HelpOptOvTarget)]SocketGuildUser target) {
|
||||
var user = await target.GetConfigAsync().ConfigureAwait(false);
|
||||
if (user.IsKnown) {
|
||||
await user.DeleteAsync().ConfigureAwait(false);
|
||||
using var db = new BotDatabaseContext();
|
||||
var user = ((SocketGuildUser)Context.User).GetUserEntryOrNew(db);
|
||||
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.")
|
||||
.ConfigureAwait(false);
|
||||
} else {
|
||||
|
|
|
@ -57,16 +57,15 @@ public class ConfigModule : BotModuleBase {
|
|||
|
||||
[SlashCommand("set-channel", HelpPfxModOnly + HelpSubCmdChannel + HelpPofxBlankUnset)]
|
||||
public async Task CmdSetChannel([Summary(description: HelpOptRole)] SocketTextChannel? channel = null) {
|
||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
||||
gconf.AnnounceChannelId = channel?.Id;
|
||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||
await DoDatabaseUpdate(Context, s => s.ChannelAnnounceId = (long?)channel?.Id);
|
||||
await RespondAsync(":white_check_mark: The announcement channel has been " +
|
||||
(channel == null ? "unset." : $"set to **{channel.Name}**."));
|
||||
}
|
||||
|
||||
[SlashCommand("set-message", HelpPfxModOnly + HelpSubCmdMessage)]
|
||||
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() {
|
||||
Label = "Single - Message for one birthday",
|
||||
|
@ -75,7 +74,7 @@ public class ConfigModule : BotModuleBase {
|
|||
MaxLength = 1500,
|
||||
Required = false,
|
||||
Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnounce,
|
||||
Value = gconf.AnnounceMessages.Item1 ?? ""
|
||||
Value = settings.AnnounceMessage ?? ""
|
||||
};
|
||||
var txtMulti = new TextInputBuilder() {
|
||||
Label = "Multi - Message for multiple birthdays",
|
||||
|
@ -84,7 +83,7 @@ public class ConfigModule : BotModuleBase {
|
|||
MaxLength = 1500,
|
||||
Required = false,
|
||||
Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnouncePl,
|
||||
Value = gconf.AnnounceMessages.Item2 ?? ""
|
||||
Value = settings.AnnounceMessagePl ?? ""
|
||||
};
|
||||
|
||||
var form = new ModalBuilder()
|
||||
|
@ -104,18 +103,18 @@ public class ConfigModule : BotModuleBase {
|
|||
if (string.IsNullOrWhiteSpace(newSingle)) newSingle = null;
|
||||
if (string.IsNullOrWhiteSpace(newMulti)) newMulti = null;
|
||||
|
||||
var gconf = await channel.Guild.GetConfigAsync().ConfigureAwait(false);
|
||||
gconf.AnnounceMessages = (newSingle, newMulti);
|
||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||
|
||||
using var db = new BotDatabaseContext();
|
||||
var settings = channel.Guild.GetConfigOrNew(db);
|
||||
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.");
|
||||
}
|
||||
|
||||
[SlashCommand("set-ping", HelpPfxModOnly + HelpSubCmdPing)]
|
||||
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);
|
||||
gconf.AnnouncePing = option;
|
||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||
await DoDatabaseUpdate(Context, s => s.AnnouncePing = option);
|
||||
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 {
|
||||
[SlashCommand("set-birthday-role", HelpPfxModOnly + "Set the role given to users having a birthday.")]
|
||||
public async Task CmdSetBRole([Summary(description: HelpOptRole)] SocketRole role) {
|
||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
||||
gconf.RoleId = role.Id;
|
||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||
await DoDatabaseUpdate(Context, s => s.RoleId = (long)role.Id);
|
||||
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)]
|
||||
public async Task CmdSetModRole([Summary(description: HelpOptRole)]SocketRole? role = null) {
|
||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
||||
gconf.ModeratorRole = role?.Id;
|
||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||
await DoDatabaseUpdate(Context, s => s.ModeratorRole = (long?)role?.Id);
|
||||
await RespondAsync(":white_check_mark: The moderator role has been " +
|
||||
(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);
|
||||
|
||||
private async Task UpdateBlockAsync(SocketGuildUser user, bool setting) {
|
||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
||||
bool already = setting == await gconf.IsUserBlockedAsync(user.Id).ConfigureAwait(false);
|
||||
// setting: true to add (set), false to remove (unset)
|
||||
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) {
|
||||
await RespondAsync($":white_check_mark: User is already {(setting ? "" : "not ")}blocked.").ConfigureAwait(false);
|
||||
} else {
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
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.")]
|
||||
public async Task CmdAddBlock([Summary(name: "enable", description: "The moderated mode setting.")] bool setting) {
|
||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
||||
bool already = setting == gconf.IsModerated;
|
||||
public async Task CmdSetModerated([Summary(name: "enable", description: "The moderated mode setting.")] bool setting) {
|
||||
bool current = false;
|
||||
await DoDatabaseUpdate(Context, s => {
|
||||
current = s.Moderated;
|
||||
s.Moderated = setting;
|
||||
});
|
||||
|
||||
bool already = setting == current;
|
||||
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 {
|
||||
gconf.IsModerated = setting;
|
||||
await gconf.UpdateAsync().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)]
|
||||
public async Task CmdCheck() {
|
||||
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;
|
||||
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($"Number of registered birthdays: `{ usercfgs.Count() }`");
|
||||
result.AppendLine($"Server time zone: `{ (conf?.TimeZone ?? "Not set - using UTC") }`");
|
||||
result.AppendLine($"Number of registered birthdays: `{ guildconf.UserEntries.Count() }`");
|
||||
result.AppendLine($"Server time zone: `{ (guildconf.TimeZone ?? "Not set - using UTC") }`");
|
||||
result.AppendLine();
|
||||
|
||||
bool hasMembers = Common.HasMostMembersDownloaded(guild);
|
||||
|
@ -194,7 +203,7 @@ public class ConfigModule : BotModuleBase {
|
|||
int bdayCount = -1;
|
||||
result.Append(DoTestFor("Birthday processing", delegate {
|
||||
if (!hasMembers) return false;
|
||||
bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(usercfgs, conf?.TimeZone).Count;
|
||||
bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(guildconf.UserEntries, guildconf.TimeZone).Count;
|
||||
return true;
|
||||
}));
|
||||
if (hasMembers) result.AppendLine($" - `{bdayCount}` user(s) currently having a birthday.");
|
||||
|
@ -202,13 +211,13 @@ public class ConfigModule : BotModuleBase {
|
|||
result.AppendLine();
|
||||
|
||||
result.AppendLine(DoTestFor("Birthday role set with `bb.config role`", delegate {
|
||||
if (conf == null) return false;
|
||||
SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
|
||||
if (guildconf.IsNew) return false;
|
||||
SocketRole? role = guild.GetRole((ulong)(guildconf.RoleId ?? 0));
|
||||
return role != null;
|
||||
}));
|
||||
result.AppendLine(DoTestFor("Birthday role can be managed by bot", delegate {
|
||||
if (conf == null) return false;
|
||||
SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
|
||||
if (guildconf.IsNew) return false;
|
||||
SocketRole? role = guild.GetRole((ulong)(guildconf.RoleId ?? 0));
|
||||
if (role == null) return false;
|
||||
return guild.CurrentUser.GuildPermissions.ManageRoles && role.Position < guild.CurrentUser.Hierarchy;
|
||||
}));
|
||||
|
@ -216,8 +225,8 @@ public class ConfigModule : BotModuleBase {
|
|||
|
||||
SocketTextChannel? announcech = null;
|
||||
result.AppendLine(DoTestFor("(Optional) Announcement channel set with `bb.config channel`", delegate {
|
||||
if (conf == null) return false;
|
||||
announcech = guild.GetTextChannel(conf.AnnounceChannelId ?? 0);
|
||||
if (guildconf.IsNew) return false;
|
||||
announcech = guild.GetTextChannel((ulong)(guildconf.ChannelAnnounceId ?? 0));
|
||||
return announcech != null;
|
||||
}));
|
||||
string disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>";
|
||||
|
@ -239,14 +248,14 @@ public class ConfigModule : BotModuleBase {
|
|||
result.AppendLine($"> {line}");
|
||||
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 dispAnnounces = new StringBuilder("Custom announcement message(s):\n");
|
||||
if (conf.AnnounceMessages.Item1 != null) {
|
||||
em = em.AddField("Single", prepareAnnouncePreview(conf.AnnounceMessages.Item1));
|
||||
if (guildconf.AnnounceMessage != null) {
|
||||
em = em.AddField("Single", prepareAnnouncePreview(guildconf.AnnounceMessage));
|
||||
}
|
||||
if (conf.AnnounceMessages.Item2 != null) {
|
||||
em = em.AddField("Multi", prepareAnnouncePreview(conf.AnnounceMessages.Item2));
|
||||
if (guildconf.AnnounceMessagePl != null) {
|
||||
em = em.AddField("Multi", prepareAnnouncePreview(guildconf.AnnounceMessagePl));
|
||||
}
|
||||
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)]
|
||||
public async Task CmdSetTimezone([Summary(description: HelpOptZone)] string? zone = null) {
|
||||
const string Response = ":white_check_mark: The server's time zone has been ";
|
||||
var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false);
|
||||
|
||||
if (zone == null) {
|
||||
gconf.TimeZone = null;
|
||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||
await DoDatabaseUpdate(Context, s => s.TimeZone = null);
|
||||
await RespondAsync(Response + "unset.").ConfigureAwait(false);
|
||||
} else {
|
||||
string parsedZone;
|
||||
|
@ -270,9 +277,22 @@ public class ConfigModule : BotModuleBase {
|
|||
return;
|
||||
}
|
||||
|
||||
gconf.TimeZone = parsedZone;
|
||||
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||
await RespondAsync(Response + $"set to **{zone}**.").ConfigureAwait(false);
|
||||
await DoDatabaseUpdate(Context, s => s.TimeZone = parsedZone);
|
||||
await RespondAsync(Response + $"set to **{parsedZone}**.").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 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) {
|
||||
// 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
|
||||
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
|
||||
if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value)) return PreconditionResult.FromSuccess();
|
||||
// Check for moderated mode
|
||||
if (settings.ModMode) return Task.FromResult(PreconditionResult.FromError(FailModerated));
|
||||
|
||||
// Moderated mode check
|
||||
if (gconf.IsModerated) return PreconditionResult.FromError(FailModerated);
|
||||
// Check if user exists in blocklist
|
||||
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
|
||||
if (await gconf.IsUserInBlocklistAsync(user.Id)) return PreconditionResult.FromError(FailBlocked);
|
||||
|
||||
return PreconditionResult.FromSuccess();
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
}
|
||||
}
|
|
@ -13,17 +13,21 @@ class RequireBotModeratorAttribute : PreconditionAttribute {
|
|||
|
||||
public override string ErrorMessage => Error;
|
||||
|
||||
public override async Task<PreconditionResult> CheckRequirementsAsync(
|
||||
public override Task<PreconditionResult> CheckRequirementsAsync(
|
||||
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
|
||||
// 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...
|
||||
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();
|
||||
var gconf = await ((SocketGuild)context.Guild).GetConfigAsync().ConfigureAwait(false);
|
||||
if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value))
|
||||
return PreconditionResult.FromSuccess();
|
||||
if (user.GuildPermissions.ManageGuild) return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
using var db = new BotDatabaseContext();
|
||||
var checkRole = (ulong?)db.GuildConfigurations
|
||||
.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 override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||
// Has the potential to disconnect while in the middle of processing.
|
||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) return;
|
||||
using var db = new BotDatabaseContext();
|
||||
|
||||
// Determine if there is action to be taken...
|
||||
if (!guild.HasAllMembers && await GuildUserAnyAsync(guild.Id).ConfigureAwait(false)) {
|
||||
await guild.DownloadUsersAsync().ConfigureAwait(false); // This is already on a separate thread; no need to Task.Run
|
||||
await Task.Delay(200, CancellationToken.None).ConfigureAwait(false); // Must delay, or else it seems to hang...
|
||||
}
|
||||
// Take action if a guild's cache is incomplete...
|
||||
var incompleteCaches = ShardInstance.DiscordClient.Guilds.Where(g => !g.HasAllMembers).Select(g => (long)g.Id).ToHashSet();
|
||||
// ...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...
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
if (processed > 100) Log($"Explicit user list request processed for {processed} guild(s).");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot.BackgroundServices;
|
||||
namespace BirthdayBot.BackgroundServices;
|
||||
|
||||
abstract class BackgroundService {
|
||||
protected ShardInstance ShardInstance { get; }
|
||||
|
|
|
@ -15,71 +15,61 @@ class BirthdayRoleUpdate : BackgroundService {
|
|||
/// Processes birthday updates for all available guilds synchronously.
|
||||
/// </summary>
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
var exs = new List<Exception>();
|
||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||
if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) {
|
||||
Log("Client is not connected. Stopping early.");
|
||||
return;
|
||||
}
|
||||
// For database efficiency, fetch all database information at once before proceeding
|
||||
using var db = new BotDatabaseContext();
|
||||
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => (long)g.Id).ToHashSet();
|
||||
var presentGuildSettings = db.GuildConfigurations.Where(s => shardGuilds.Contains(s.GuildId));
|
||||
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.
|
||||
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
||||
|
||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) {
|
||||
Log("Client is not connected. Stopping early.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ProcessGuildAsync(guild).ConfigureAwait(false);
|
||||
// Verify that role settings and permissions are usable
|
||||
SocketRole? role = guild.GetRole((ulong)(settings.RoleId ?? 0));
|
||||
if (role == null
|
||||
|| !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()) {
|
||||
await AnnounceBirthdaysAsync(settings, guild, announcementList);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Catch all exceptions per-guild but continue processing, throw at end.
|
||||
exs.Add(ex);
|
||||
exceptions.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;
|
||||
}
|
||||
|
||||
// Birthday announcement
|
||||
var announce = gc.AnnounceMessages;
|
||||
var announceping = gc.AnnouncePing;
|
||||
SocketTextChannel? channel = null;
|
||||
if (gc.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gc.AnnounceChannelId.Value);
|
||||
if (announcementList.Any()) {
|
||||
await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList).ConfigureAwait(false);
|
||||
}
|
||||
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>
|
||||
/// 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>
|
||||
[Obsolete(Database.ObsoleteReason)]
|
||||
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string? defaultTzStr) {
|
||||
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>();
|
||||
foreach (var item in guildUsers) {
|
||||
|
@ -101,39 +91,64 @@ class BirthdayRoleUpdate : BackgroundService {
|
|||
}
|
||||
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>
|
||||
/// Sets the birthday role to all applicable users. Unsets it from all others who may have it.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// First item: 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.
|
||||
/// List of users who had the birthday role applied, used to announce.
|
||||
/// </returns>
|
||||
private static async Task<(IEnumerable<SocketGuildUser>, (int, int))> UpdateGuildBirthdayRoles(
|
||||
SocketGuild g, SocketRole r, HashSet<ulong> names) {
|
||||
// Check members currently with the role. Figure out which users to remove it from.
|
||||
var roleRemoves = new List<SocketGuildUser>();
|
||||
var roleKeeps = new HashSet<ulong>();
|
||||
foreach (var member in r.Members) {
|
||||
if (!names.Contains(member.Id)) roleRemoves.Add(member);
|
||||
else roleKeeps.Add(member.Id);
|
||||
}
|
||||
private static async Task<IEnumerable<SocketGuildUser>> UpdateGuildBirthdayRoles(SocketGuild g, SocketRole r, HashSet<ulong> toApply) {
|
||||
var additions = new List<SocketGuildUser>();
|
||||
try {
|
||||
var removals = new List<SocketGuildUser>(); // TODO check if roles can be removed in-place instead of building a list first
|
||||
var no_ops = new HashSet<ulong>();
|
||||
|
||||
foreach (var user in roleRemoves) {
|
||||
await user.RemoveRoleAsync(r).ConfigureAwait(false);
|
||||
}
|
||||
// Scan role for members no longer needing it
|
||||
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);
|
||||
}
|
||||
|
||||
// Apply role to members not already having it. Prepare announcement list.
|
||||
var newBirthdays = new List<SocketGuildUser>();
|
||||
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);
|
||||
foreach (var target in toApply) {
|
||||
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)
|
||||
when (ex.DiscordCode is DiscordErrorCode.MissingPermissions or DiscordErrorCode.InsufficientPermissions) {
|
||||
// Encountered access and/or permission issues despite earlier checks. Quit the loop here.
|
||||
}
|
||||
|
||||
return (newBirthdays, (newBirthdays.Count, roleRemoves.Count));
|
||||
return additions;
|
||||
}
|
||||
|
||||
public const string DefaultAnnounce = "Please wish a happy birthday to %n!";
|
||||
|
@ -142,21 +157,21 @@ class BirthdayRoleUpdate : BackgroundService {
|
|||
/// <summary>
|
||||
/// Attempts to send an announcement message.
|
||||
/// </summary>
|
||||
private static async Task AnnounceBirthdaysAsync(
|
||||
(string?, string?) announce, bool announcePing, SocketTextChannel? c, IEnumerable<SocketGuildUser> names) {
|
||||
internal static async Task AnnounceBirthdaysAsync(GuildConfig settings, SocketGuild g, IEnumerable<SocketGuildUser> names) {
|
||||
var c = g.GetTextChannel((ulong)(settings.ChannelAnnounceId ?? 0));
|
||||
if (c == null) return;
|
||||
if (!c.Guild.CurrentUser.GetPermissions(c).SendMessages) return;
|
||||
|
||||
string announceMsg;
|
||||
if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce;
|
||||
else announceMsg = announce.Item2 ?? announce.Item1 ?? DefaultAnnouncePl;
|
||||
if (names.Count() == 1) announceMsg = settings.AnnounceMessage ?? settings.AnnounceMessagePl ?? DefaultAnnounce;
|
||||
else announceMsg = settings.AnnounceMessagePl ?? settings.AnnounceMessage ?? DefaultAnnouncePl;
|
||||
announceMsg = announceMsg.TrimEnd();
|
||||
if (!announceMsg.Contains("%n")) announceMsg += " %n";
|
||||
|
||||
// Build sorted name list
|
||||
var namestrings = new List<string>();
|
||||
foreach (var item in names)
|
||||
namestrings.Add(Common.FormatName(item, announcePing));
|
||||
namestrings.Add(Common.FormatName(item, settings.AnnouncePing));
|
||||
namestrings.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var namedisplay = new StringBuilder();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using BirthdayBot.Data;
|
||||
using NpgsqlTypes;
|
||||
using System.Text;
|
||||
|
||||
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.
|
||||
if ((tickCount + ShardInstance.ShardId) % ProcessInterval != 0) return;
|
||||
|
||||
try {
|
||||
// A semaphore is used to restrict this work being done concurrently on other shards
|
||||
// to avoid putting pressure on the SQL connection pool. Clearing old database information
|
||||
// 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();
|
||||
using var db = new BotDatabaseContext();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
int updatedGuilds = 0, updatedUsers = 0;
|
||||
|
||||
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++;
|
||||
|
||||
// Update users
|
||||
var localIds = guild.Users.Select(u => (long)u.Id);
|
||||
var dbSavedIds = db.UserEntries.Where(e => e.GuildId == (long)guild.Id).Select(e => e.UserId);
|
||||
var usersToUpdate = localIds.Intersect(dbSavedIds).ToHashSet();
|
||||
foreach (var user in db.UserEntries.Where(e => e.GuildId == (long)guild.Id && usersToUpdate.Contains(e.UserId))) {
|
||||
user.LastSeen = now;
|
||||
updatedUsers++;
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
// 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();
|
||||
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");
|
||||
if (staleGuildCount != 0 || staleUserCount != 0) {
|
||||
resultText.Append(" Discarded ");
|
||||
if (staleGuildCount != 0) {
|
||||
resultText.Append($"{staleGuildCount} guilds");
|
||||
if (staleUserCount != 0) resultText.Append(", ");
|
||||
}
|
||||
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
|
||||
// Statement for updating last_seen in guilds
|
||||
var cUpdateGuild = db.CreateCommand();
|
||||
cUpdateGuild.CommandText = $"update {GuildConfiguration.BackingTable} set last_seen = now() "
|
||||
+ "where guild_id = @Gid";
|
||||
var pUpdateG = cUpdateGuild.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||
cUpdateGuild.Prepare();
|
||||
|
||||
// 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);
|
||||
if (staleUserCount != 0) {
|
||||
resultText.Append($"{staleUserCount} users");
|
||||
}
|
||||
var resultText = new StringBuilder();
|
||||
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");
|
||||
|
||||
// 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 ");
|
||||
if (staleGuilds != 0) {
|
||||
resultText.Append($"{staleGuilds} guilds");
|
||||
if (staleUsers != 0) resultText.Append(", ");
|
||||
}
|
||||
if (staleUsers != 0) {
|
||||
resultText.Append($"{staleUsers} users");
|
||||
}
|
||||
resultText.Append('.');
|
||||
}
|
||||
Log(resultText.ToString());
|
||||
} finally {
|
||||
_updateLock.Release();
|
||||
resultText.Append('.');
|
||||
}
|
||||
Log(resultText.ToString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
|
||||
namespace BirthdayBot.BackgroundServices;
|
||||
|
||||
|
@ -11,7 +7,7 @@ namespace BirthdayBot.BackgroundServices;
|
|||
/// </summary>
|
||||
class ExternalStatisticsReporting : BackgroundService {
|
||||
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();
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ class ShardBackgroundWorker : IDisposable {
|
|||
_tickCount++;
|
||||
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
|
||||
} 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;
|
||||
|
|
|
@ -23,10 +23,17 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||
<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="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NodaTime" Version="3.0.9" />
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -27,6 +27,8 @@ class Configuration {
|
|||
public int ShardAmount { get; }
|
||||
public int ShardTotal { get; }
|
||||
|
||||
public string DatabaseConnectionString { get; }
|
||||
|
||||
public Configuration(string[] args) {
|
||||
var cmdline = CmdLineOpts.Parse(args);
|
||||
|
||||
|
@ -74,7 +76,7 @@ class Configuration {
|
|||
};
|
||||
var sqldb = ReadConfKey<string>(jc, KeySqlDatabase, false);
|
||||
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) {
|
||||
|
|
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 System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot.Data;
|
||||
|
||||
[Obsolete(ObsoleteReason, error: false)]
|
||||
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>
|
||||
/// Sets up and opens a database connection.
|
||||
|
|
|
@ -2,20 +2,18 @@
|
|||
|
||||
internal static class Extensions {
|
||||
/// <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>
|
||||
internal static async Task<GuildConfiguration> GetConfigAsync(this SocketGuild guild)
|
||||
=> await GuildConfiguration.LoadAsync(guild.Id, false);
|
||||
public static GuildConfig GetConfigOrNew(this SocketGuild guild, BotDatabaseContext db)
|
||||
=> db.GuildConfigurations.Where(g => g.GuildId == (long)guild.Id).FirstOrDefault()
|
||||
?? new GuildConfig() { IsNew = true, GuildId = (long)guild.Id };
|
||||
|
||||
/// <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>
|
||||
internal static async Task<IEnumerable<GuildUserConfiguration>> GetUserConfigurationsAsync(this SocketGuild guild)
|
||||
=> await GuildUserConfiguration.LoadAllAsync(guild.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);
|
||||
public static UserEntry GetUserEntryOrNew(this SocketGuildUser user, BotDatabaseContext db)
|
||||
=> 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 };
|
||||
}
|
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.
|
||||
/// Updating any property requires a call to <see cref="UpdateAsync"/> for changes to take effect.
|
||||
/// </summary>
|
||||
[Obsolete(Database.ObsoleteReason, error: false)]
|
||||
class GuildConfiguration {
|
||||
/// <summary>
|
||||
/// Gets this configuration's corresponding guild ID.
|
||||
|
@ -73,7 +74,6 @@ class GuildConfiguration {
|
|||
/// <summary>
|
||||
/// Checks if the specified user is blocked by current guild policy (block list or moderated mode).
|
||||
/// </summary>
|
||||
[Obsolete("Block lists should be reimplemented in a more resource-efficient manner later.", false)]
|
||||
public async Task<bool> IsUserBlockedAsync(ulong userId) {
|
||||
if (IsModerated) return true;
|
||||
return await IsUserInBlocklistAsync(userId).ConfigureAwait(false);
|
||||
|
@ -82,7 +82,6 @@ class GuildConfiguration {
|
|||
/// <summary>
|
||||
/// Checks if the given user exists in the block list.
|
||||
/// </summary>
|
||||
[Obsolete("Block lists should be reimplemented in a more resource-efficient manner later.", false)]
|
||||
public async Task<bool> IsUserInBlocklistAsync(ulong userId) {
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
using var c = db.CreateCommand();
|
||||
|
@ -99,7 +98,6 @@ class GuildConfiguration {
|
|||
/// <summary>
|
||||
/// Adds the specified user to the block list corresponding to this guild.
|
||||
/// </summary>
|
||||
[Obsolete("Block lists will be reimplemented in a more practical manner later.", false)]
|
||||
public async Task BlockUserAsync(ulong userId) {
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
using var c = db.CreateCommand();
|
||||
|
@ -117,7 +115,6 @@ class GuildConfiguration {
|
|||
/// Removes the specified user from the block list corresponding to this guild.
|
||||
/// </summary>
|
||||
/// <returns>True if a user has been removed, false if the requested user was not in this list.</returns>
|
||||
[Obsolete("Block lists will be reimplemented in a more practical manner later.", false)]
|
||||
public async Task<bool> UnblockUserAsync(ulong userId) {
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
using var c = db.CreateCommand();
|
||||
|
@ -134,7 +131,6 @@ class GuildConfiguration {
|
|||
/// Checks if the given user can be considered a bot moderator.
|
||||
/// Checks for either the Manage Guild permission or if the user is within a predetermined role.
|
||||
/// </summary>
|
||||
[Obsolete("Usage should be phased out when text commands are removed. Use PreconditionAttribute from now on.", error: false)]
|
||||
public bool IsBotModerator(SocketGuildUser user)
|
||||
=> 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 BackingTableBans = "banned_users";
|
||||
|
||||
[Obsolete("DELETE THIS", error: true)]
|
||||
internal static async Task DatabaseSetupAsync(NpgsqlConnection db) {
|
||||
using (var c = db.CreateCommand()) {
|
||||
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
|
||||
/// not exist in the database.
|
||||
/// </param>
|
||||
[Obsolete("Begin using extension method to retrieve necessary data instead.", false)]
|
||||
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?
|
||||
using (var db = await Database.OpenConnectionAsync().ConfigureAwait(false)) {
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace BirthdayBot.Data;
|
|||
/// <summary>
|
||||
/// Represents configuration for a guild user as may exist in the database.
|
||||
/// </summary>
|
||||
[Obsolete(Database.ObsoleteReason, error: false)]
|
||||
class GuildUserConfiguration {
|
||||
public ulong GuildId { get; }
|
||||
public ulong UserId { get; }
|
||||
|
@ -106,7 +107,6 @@ class GuildUserConfiguration {
|
|||
/// <summary>
|
||||
/// Attempts to retrieve a user's configuration. Returns a new, updateable instance if none is found.
|
||||
/// </summary>
|
||||
[Obsolete("Migrate to using extension methods to retrieve necessary data instead.", false)]
|
||||
public static async Task<GuildUserConfiguration> LoadAsync(ulong guildId, ulong userId) {
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
using var c = db.CreateCommand();
|
||||
|
@ -123,7 +123,6 @@ class GuildUserConfiguration {
|
|||
/// <summary>
|
||||
/// Gets all known user configuration records associated with the specified guild.
|
||||
/// </summary>
|
||||
[Obsolete("Migrate to using extension methods to retrieve necessary data instead.", false)]
|
||||
public static async Task<IEnumerable<GuildUserConfiguration>> LoadAllAsync(ulong guildId) {
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
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 System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot;
|
||||
|
||||
|
@ -22,6 +20,9 @@ class Program {
|
|||
Environment.Exit((int)ExitCodes.ConfigError);
|
||||
}
|
||||
|
||||
BotDatabaseContext.NpgsqlConnectionString = cfg.DatabaseConnectionString;
|
||||
|
||||
Database.DBConnectionString = cfg.DatabaseConnectionString;
|
||||
try {
|
||||
await Database.DoInitialDatabaseSetupAsync();
|
||||
} catch (Npgsql.NpgsqlException e) {
|
||||
|
|
|
@ -127,6 +127,7 @@ public sealed class ShardInstance : IDisposable {
|
|||
#endif
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
/// <summary>
|
||||
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
|
||||
/// </summary>
|
||||
|
@ -166,6 +167,7 @@ public sealed class ShardInstance : IDisposable {
|
|||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// Slash command preparation and invocation
|
||||
private async Task DiscordClient_InteractionCreated(SocketInteraction arg) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using BirthdayBot.Data;
|
||||
#pragma warning disable CS0618
|
||||
using BirthdayBot.Data;
|
||||
using NodaTime;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.RegularExpressions;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using BirthdayBot.Data;
|
||||
#pragma warning disable CS0618
|
||||
using BirthdayBot.Data;
|
||||
using System.Text;
|
||||
|
||||
namespace BirthdayBot.TextCommands;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using BirthdayBot.Data;
|
||||
#pragma warning disable CS0618
|
||||
using BirthdayBot.Data;
|
||||
using System.Text;
|
||||
|
||||
namespace BirthdayBot.TextCommands;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using BirthdayBot.Data;
|
||||
#pragma warning disable CS0618
|
||||
using BirthdayBot.Data;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using BirthdayBot.Data;
|
||||
#pragma warning disable CS0618
|
||||
using BirthdayBot.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BirthdayBot.TextCommands;
|
||||
|
|
Loading…
Reference in a new issue