mirror of
https://github.com/NoiTheCat/WorldTime.git
synced 2024-11-21 14:34:36 +00:00
Merge pull request #11 from NoiTheCat/efcore
Switch to Entity Framework Core
This commit is contained in:
commit
6ceb995bbe
13 changed files with 414 additions and 190 deletions
|
@ -3,9 +3,9 @@ using NodaTime;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using WorldTime.Data;
|
||||||
|
|
||||||
namespace WorldTime;
|
namespace WorldTime;
|
||||||
|
|
||||||
public class ApplicationCommands : InteractionModuleBase<ShardedInteractionContext> {
|
public class ApplicationCommands : InteractionModuleBase<ShardedInteractionContext> {
|
||||||
const string ErrNotAllowed = ":x: Only server moderators may use this command.";
|
const string ErrNotAllowed = ":x: Only server moderators may use this command.";
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
||||||
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
||||||
|
|
||||||
public DiscordShardedClient ShardedClient { get; set; } = null!;
|
public DiscordShardedClient ShardedClient { get; set; } = null!;
|
||||||
public Database Database { get; set; } = null!;
|
public BotDatabaseContext DbContext { get; set; } = null!;
|
||||||
|
|
||||||
static ApplicationCommands() {
|
static ApplicationCommands() {
|
||||||
Dictionary<string, string> tzNameMap = new(StringComparer.OrdinalIgnoreCase);
|
Dictionary<string, string> tzNameMap = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
@ -42,7 +42,8 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
||||||
public async Task CmdHelp() {
|
public async Task CmdHelp() {
|
||||||
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
|
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
|
||||||
var guildct = ShardedClient.Guilds.Count;
|
var guildct = ShardedClient.Guilds.Count;
|
||||||
var uniquetz = await Database.GetDistinctZoneCountAsync();
|
using var db = DbContext;
|
||||||
|
var uniquetz = db.GetDistinctZoneCount();
|
||||||
await RespondAsync(embed: new EmbedBuilder() {
|
await RespondAsync(embed: new EmbedBuilder() {
|
||||||
Title = "Help & About",
|
Title = "Help & About",
|
||||||
Description = $"World Time v{version} - Serving {guildct} communities across {uniquetz} time zones.\n\n"
|
Description = $"World Time v{version} - Serving {guildct} communities across {uniquetz} time zones.\n\n"
|
||||||
|
@ -69,12 +70,19 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user != null) {
|
if (user == null) {
|
||||||
await CmdListUserAsync(user);
|
// No parameter - full listing
|
||||||
return;
|
await CmdListWithoutParamAsync();
|
||||||
|
} else {
|
||||||
|
// Has parameter - do single user listing
|
||||||
|
await CmdListWithUserParamAsync(user);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var userlist = await Database.GetGuildZonesAsync(Context.Guild.Id);
|
private async Task CmdListWithoutParamAsync() {
|
||||||
|
// Called by CmdList
|
||||||
|
using var db = DbContext;
|
||||||
|
var userlist = db.GetGuildZones(Context.Guild.Id);
|
||||||
if (userlist.Count == 0) {
|
if (userlist.Count == 0) {
|
||||||
await RespondAsync(":x: Nothing to show. Register your time zones with the bot using the `/set` command.");
|
await RespondAsync(":x: Nothing to show. Register your time zones with the bot using the `/set` command.");
|
||||||
return;
|
return;
|
||||||
|
@ -138,9 +146,10 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdListUserAsync(SocketGuildUser parameter) {
|
private async Task CmdListWithUserParamAsync(SocketGuildUser parameter) {
|
||||||
// Not meant as a command handler - called by CmdList
|
// Called by CmdList
|
||||||
var result = await Database.GetUserZoneAsync(parameter);
|
using var db = DbContext;
|
||||||
|
var result = db.GetUserZone(parameter);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
bool isself = Context.User.Id == parameter.Id;
|
bool isself = Context.User.Id == parameter.Id;
|
||||||
if (isself) await RespondAsync(":x: You do not have a time zone. Set it with `tz.set`.", ephemeral: true);
|
if (isself) await RespondAsync(":x: You do not have a time zone. Set it with `tz.set`.", ephemeral: true);
|
||||||
|
@ -159,7 +168,8 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
||||||
await RespondAsync(ErrInvalidZone, ephemeral: true);
|
await RespondAsync(ErrInvalidZone, ephemeral: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Database.UpdateUserAsync((SocketGuildUser)Context.User, parsedzone);
|
using var db = DbContext;
|
||||||
|
db.UpdateUser((SocketGuildUser)Context.User, parsedzone);
|
||||||
await RespondAsync($":white_check_mark: Your time zone has been set to **{parsedzone}**.");
|
await RespondAsync($":white_check_mark: Your time zone has been set to **{parsedzone}**.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,14 +189,16 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Database.UpdateUserAsync(user, newtz).ConfigureAwait(false);
|
using var db = DbContext;
|
||||||
|
db.UpdateUser(user, newtz);
|
||||||
await RespondAsync($":white_check_mark: Time zone for **{user}** set to **{newtz}**.");
|
await RespondAsync($":white_check_mark: Time zone for **{user}** set to **{newtz}**.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireGuildContext]
|
[RequireGuildContext]
|
||||||
[SlashCommand("remove", HelpRemove)]
|
[SlashCommand("remove", HelpRemove)]
|
||||||
public async Task CmdRemove() {
|
public async Task CmdRemove() {
|
||||||
var success = await Database.DeleteUserAsync((SocketGuildUser)Context.User);
|
using var db = DbContext;
|
||||||
|
var success = db.DeleteUser((SocketGuildUser)Context.User);
|
||||||
if (success) await RespondAsync(":white_check_mark: Your zone has been removed.");
|
if (success) await RespondAsync(":white_check_mark: Your zone has been removed.");
|
||||||
else await RespondAsync(":x: You don't have a time zone set.");
|
else await RespondAsync(":x: You don't have a time zone set.");
|
||||||
}
|
}
|
||||||
|
@ -199,7 +211,8 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await Database.DeleteUserAsync(user))
|
using var db = DbContext;
|
||||||
|
if (db.DeleteUser(user))
|
||||||
await RespondAsync($":white_check_mark: Removed zone information for {user}.");
|
await RespondAsync($":white_check_mark: Removed zone information for {user}.");
|
||||||
else
|
else
|
||||||
await RespondAsync($":white_check_mark: No time zone is set for {user}.");
|
await RespondAsync($":white_check_mark: No time zone is set for {user}.");
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
using NodaTime;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NodaTime;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using WorldTime.Data;
|
||||||
|
|
||||||
namespace WorldTime;
|
namespace WorldTime;
|
||||||
|
[Obsolete("Text commands are deprecated and will be removed soon.")]
|
||||||
internal class CommandsText {
|
internal class CommandsText {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
public const string CommandPrefix = "tt.";
|
public const string CommandPrefix = "tt.";
|
||||||
|
@ -15,9 +17,11 @@ internal class CommandsText {
|
||||||
delegate Task Command(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message);
|
delegate Task Command(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message);
|
||||||
|
|
||||||
private readonly Dictionary<string, Command> _commands;
|
private readonly Dictionary<string, Command> _commands;
|
||||||
private readonly Database _database;
|
private readonly IServiceProvider _services;
|
||||||
private readonly WorldTime _instance;
|
private readonly WorldTime _instance;
|
||||||
|
|
||||||
|
private BotDatabaseContext Database => _services.GetRequiredService<BotDatabaseContext>();
|
||||||
|
|
||||||
private static readonly Regex _userExplicit;
|
private static readonly Regex _userExplicit;
|
||||||
private static readonly Regex _userMention;
|
private static readonly Regex _userMention;
|
||||||
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
||||||
|
@ -38,9 +42,9 @@ internal class CommandsText {
|
||||||
_tzNameMap = new(tzNameMap);
|
_tzNameMap = new(tzNameMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandsText(WorldTime inst, Database db) {
|
public CommandsText(WorldTime inst, IServiceProvider services) {
|
||||||
_instance = inst;
|
_instance = inst;
|
||||||
_database = db;
|
_services = services;
|
||||||
_commands = new(StringComparer.OrdinalIgnoreCase) {
|
_commands = new(StringComparer.OrdinalIgnoreCase) {
|
||||||
{ "help", CmdHelp },
|
{ "help", CmdHelp },
|
||||||
{ "list", CmdList },
|
{ "list", CmdList },
|
||||||
|
@ -60,7 +64,7 @@ internal class CommandsText {
|
||||||
|
|
||||||
var msgsplit = message.Content.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
var msgsplit = message.Content.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (msgsplit.Length == 0 || msgsplit[0].Length < 4) return;
|
if (msgsplit.Length == 0 || msgsplit[0].Length < 4) return;
|
||||||
if (msgsplit[0].StartsWith(CommandPrefix, StringComparison.OrdinalIgnoreCase)) { // TODO add support for multiple prefixes?
|
if (msgsplit[0].StartsWith(CommandPrefix, StringComparison.OrdinalIgnoreCase)) {
|
||||||
var cmdBase = msgsplit[0][3..];
|
var cmdBase = msgsplit[0][3..];
|
||||||
if (_commands.ContainsKey(cmdBase)) {
|
if (_commands.ContainsKey(cmdBase)) {
|
||||||
Program.Log("Command invoked", $"{channel.Guild.Name}/{message.Author} {message.Content}");
|
Program.Log("Command invoked", $"{channel.Guild.Name}/{message.Author} {message.Content}");
|
||||||
|
@ -74,9 +78,10 @@ internal class CommandsText {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdHelp(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
|
private async Task CmdHelp(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
|
||||||
|
using var db = Database;
|
||||||
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
|
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
|
||||||
var guildct = _instance.DiscordClient.Guilds.Count;
|
var guildct = _instance.DiscordClient.Guilds.Count;
|
||||||
var uniquetz = await _database.GetDistinctZoneCountAsync();
|
var uniquetz = db.GetDistinctZoneCount();
|
||||||
await channel.SendMessageAsync(embed: new EmbedBuilder() {
|
await channel.SendMessageAsync(embed: new EmbedBuilder() {
|
||||||
Color = new Color(0xe0f2f7),
|
Color = new Color(0xe0f2f7),
|
||||||
Title = "Help & About",
|
Title = "Help & About",
|
||||||
|
@ -118,7 +123,8 @@ internal class CommandsText {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _database.GetUserZoneAsync(usersearch).ConfigureAwait(false);
|
using var db = Database;
|
||||||
|
var result = db.GetUserZone(usersearch);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
bool isself = sender.Id == usersearch.Id;
|
bool isself = sender.Id == usersearch.Id;
|
||||||
if (isself) await channel.SendMessageAsync(":x: You do not have a time zone. Set it with `tz.set`.").ConfigureAwait(false);
|
if (isself) await channel.SendMessageAsync(":x: You do not have a time zone. Set it with `tz.set`.").ConfigureAwait(false);
|
||||||
|
@ -130,7 +136,8 @@ internal class CommandsText {
|
||||||
await channel.SendMessageAsync(embed: new EmbedBuilder().WithDescription(resulttext).Build()).ConfigureAwait(false);
|
await channel.SendMessageAsync(embed: new EmbedBuilder().WithDescription(resulttext).Build()).ConfigureAwait(false);
|
||||||
} else {
|
} else {
|
||||||
// Does not have parameter - build full list
|
// Does not have parameter - build full list
|
||||||
var userlist = await _database.GetGuildZonesAsync(channel.Guild.Id).ConfigureAwait(false);
|
using var db = Database;
|
||||||
|
var userlist = db.GetGuildZones(channel.Guild.Id);
|
||||||
if (userlist.Count == 0) {
|
if (userlist.Count == 0) {
|
||||||
await channel.SendMessageAsync(":x: Nothing to show. " +
|
await channel.SendMessageAsync(":x: Nothing to show. " +
|
||||||
$"To register time zones with the bot, use the `{CommandPrefix}set` command.").ConfigureAwait(false);
|
$"To register time zones with the bot, use the `{CommandPrefix}set` command.").ConfigureAwait(false);
|
||||||
|
@ -196,7 +203,8 @@ internal class CommandsText {
|
||||||
await channel.SendMessageAsync(ErrInvalidZone).ConfigureAwait(false);
|
await channel.SendMessageAsync(ErrInvalidZone).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _database.UpdateUserAsync(sender, input).ConfigureAwait(false);
|
using var db = Database;
|
||||||
|
db.UpdateUser(sender, input);
|
||||||
await channel.SendMessageAsync($":white_check_mark: Your time zone has been set to **{input}**.").ConfigureAwait(false);
|
await channel.SendMessageAsync($":white_check_mark: Your time zone has been set to **{input}**.").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,12 +237,14 @@ internal class CommandsText {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _database.UpdateUserAsync(targetuser, newtz).ConfigureAwait(false);
|
using var db = Database;
|
||||||
|
db.UpdateUser(targetuser, newtz);
|
||||||
await channel.SendMessageAsync($":white_check_mark: Time zone for **{targetuser}** set to **{newtz}**.").ConfigureAwait(false);
|
await channel.SendMessageAsync($":white_check_mark: Time zone for **{targetuser}** set to **{newtz}**.").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdRemove(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
|
private async Task CmdRemove(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
|
||||||
var success = await _database.DeleteUserAsync(sender).ConfigureAwait(false);
|
using var db = Database;
|
||||||
|
var success = db.DeleteUser(sender);
|
||||||
if (success) await channel.SendMessageAsync(":white_check_mark: Your zone has been removed.").ConfigureAwait(false);
|
if (success) await channel.SendMessageAsync(":white_check_mark: Your zone has been removed.").ConfigureAwait(false);
|
||||||
else await channel.SendMessageAsync(":x: You don't have a time zone set.");
|
else await channel.SendMessageAsync(":x: You don't have a time zone set.");
|
||||||
}
|
}
|
||||||
|
@ -259,7 +269,8 @@ internal class CommandsText {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _database.DeleteUserAsync(targetuser).ConfigureAwait(false);
|
using var db = Database;
|
||||||
|
db.DeleteUser(targetuser);
|
||||||
await channel.SendMessageAsync($":white_check_mark: Removed zone information for {targetuser}.");
|
await channel.SendMessageAsync($":white_check_mark: Removed zone information for {targetuser}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,7 +357,6 @@ internal class CommandsText {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool IsUserAdmin(SocketGuildUser user)
|
private static bool IsUserAdmin(SocketGuildUser user)
|
||||||
=> user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
|
=> user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
|
||||||
// TODO port modrole feature from BB, implement in here
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed.
|
/// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed.
|
||||||
|
|
114
Data/BotDatabaseContext.cs
Normal file
114
Data/BotDatabaseContext.cs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace WorldTime.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=worldtime;Password=wt";
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
get => _npgsqlConnectionString!;
|
||||||
|
#endif
|
||||||
|
set => _npgsqlConnectionString ??= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<UserEntry>(entity => {
|
||||||
|
entity.HasKey(e => new { e.GuildId, e.UserId }).HasName("userdata_pkey");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Helper methods / abstractions
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a given guild contains at least one user data entry with recent enough activity.
|
||||||
|
/// <br />To be used within a <see langword="using"/> context.
|
||||||
|
/// </summary>
|
||||||
|
internal bool HasAnyUsers(SocketGuild guild) => UserEntries.Where(u => u.GuildId == (long)guild.Id).Any();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of unique time zones in the database.
|
||||||
|
/// <br />To be used within a <see langword="using"/> context.
|
||||||
|
/// </summary>
|
||||||
|
internal int GetDistinctZoneCount() => UserEntries.Select(u => u.TimeZone).Distinct().Count();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the specified user from the database.
|
||||||
|
/// <br />To be used within a <see langword="using"/> context.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true"/> if the removal was successful.
|
||||||
|
/// <see langword="false"/> if the user did not exist.
|
||||||
|
/// </returns>
|
||||||
|
internal bool DeleteUser(SocketGuildUser user) {
|
||||||
|
var tuser = UserEntries.Where(u => u.UserId == (long)user.Id && u.GuildId == (long)user.Guild.Id).SingleOrDefault();
|
||||||
|
if (tuser != null) {
|
||||||
|
Remove(tuser);
|
||||||
|
SaveChanges();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inserts/updates the specified user in the database.
|
||||||
|
/// <br />To be used within a <see langword="using"/> context.
|
||||||
|
/// </summary>
|
||||||
|
internal void UpdateUser(SocketGuildUser user, string timezone) {
|
||||||
|
var tuser = UserEntries.Where(u => u.UserId == (long)user.Id && u.GuildId == (long)user.Guild.Id).SingleOrDefault();
|
||||||
|
if (tuser != null) {
|
||||||
|
Update(tuser);
|
||||||
|
} else {
|
||||||
|
tuser = new UserEntry() { UserId = (long)user.Id, GuildId = (long)user.Guild.Id };
|
||||||
|
Add(tuser);
|
||||||
|
}
|
||||||
|
tuser.TimeZone = timezone;
|
||||||
|
SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the time zone name of a single user.
|
||||||
|
/// <br />To be used within a <see langword="using"/> context.
|
||||||
|
/// </summary>
|
||||||
|
internal string? GetUserZone(SocketGuildUser user) {
|
||||||
|
var tuser = UserEntries.Where(u => u.UserId == (long)user.Id && u.GuildId == (long)user.Guild.Id).SingleOrDefault();
|
||||||
|
return tuser?.TimeZone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves all known user time zones for the given guild.
|
||||||
|
/// <br />To be used within a <see langword="using"/> context.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An unsorted dictionary. Keys are time zones, values are user IDs representative of those zones.
|
||||||
|
/// </returns>
|
||||||
|
internal Dictionary<string, List<ulong>> GetGuildZones(ulong guildId) {
|
||||||
|
// Implementing the query from the previous iteration, in which further filtering is done by the caller.
|
||||||
|
// TODO consider bringing filtering back to this step, if there may be any advantage
|
||||||
|
var query = from entry in UserEntries
|
||||||
|
where entry.GuildId == (long)guildId
|
||||||
|
orderby entry.UserId
|
||||||
|
select Tuple.Create(entry.TimeZone, (ulong)entry.UserId);
|
||||||
|
var resultSet = new Dictionary<string, List<ulong>>();
|
||||||
|
foreach (var (tz, user) in query) {
|
||||||
|
if (!resultSet.ContainsKey(tz)) resultSet.Add(tz, new List<ulong>());
|
||||||
|
resultSet[tz].Add(user);
|
||||||
|
}
|
||||||
|
return resultSet;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
56
Data/Migrations/20220628235704_InitialMigration.Designer.cs
generated
Normal file
56
Data/Migrations/20220628235704_InitialMigration.Designer.cs
generated
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using WorldTime.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace WorldTime.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(BotDatabaseContext))]
|
||||||
|
[Migration("20220628235704_InitialMigration")]
|
||||||
|
partial class InitialMigration
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.6")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("WorldTime.Data.UserEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdate")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_active")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("zone");
|
||||||
|
|
||||||
|
b.HasKey("GuildId", "UserId")
|
||||||
|
.HasName("userdata_pkey");
|
||||||
|
|
||||||
|
b.ToTable("userdata", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
Data/Migrations/20220628235704_InitialMigration.cs
Normal file
33
Data/Migrations/20220628235704_InitialMigration.cs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace WorldTime.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class InitialMigration : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "userdata",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guild_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
zone = table.Column<string>(type: "text", nullable: false),
|
||||||
|
last_active = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("userdata_pkey", x => new { x.guild_id, x.user_id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "userdata");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
Data/Migrations/20220629003046_RemoveLastUpdated.Designer.cs
generated
Normal file
49
Data/Migrations/20220629003046_RemoveLastUpdated.Designer.cs
generated
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using WorldTime.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace WorldTime.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(BotDatabaseContext))]
|
||||||
|
[Migration("20220629003046_RemoveLastUpdated")]
|
||||||
|
partial class RemoveLastUpdated
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.6")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("WorldTime.Data.UserEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("zone");
|
||||||
|
|
||||||
|
b.HasKey("GuildId", "UserId")
|
||||||
|
.HasName("userdata_pkey");
|
||||||
|
|
||||||
|
b.ToTable("userdata", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
Data/Migrations/20220629003046_RemoveLastUpdated.cs
Normal file
27
Data/Migrations/20220629003046_RemoveLastUpdated.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace WorldTime.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class RemoveLastUpdated : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "last_active",
|
||||||
|
table: "userdata");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "last_active",
|
||||||
|
table: "userdata",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: false,
|
||||||
|
defaultValueSql: "now()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
Data/Migrations/BotDatabaseContextModelSnapshot.cs
Normal file
47
Data/Migrations/BotDatabaseContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using WorldTime.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace WorldTime.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.6")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("WorldTime.Data.UserEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("GuildId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("zone");
|
||||||
|
|
||||||
|
b.HasKey("GuildId", "UserId")
|
||||||
|
.HasName("userdata_pkey");
|
||||||
|
|
||||||
|
b.ToTable("userdata", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
Data/UserEntry.cs
Normal file
15
Data/UserEntry.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace WorldTime.Data;
|
||||||
|
[Table("userdata")]
|
||||||
|
public class UserEntry {
|
||||||
|
[Key]
|
||||||
|
[Column("guild_id")]
|
||||||
|
public long GuildId { get; set; }
|
||||||
|
[Key]
|
||||||
|
[Column("user_id")]
|
||||||
|
public long UserId { get; set; }
|
||||||
|
[Column("zone")]
|
||||||
|
public string TimeZone { get; set; } = null!;
|
||||||
|
}
|
144
Database.cs
144
Database.cs
|
@ -1,144 +0,0 @@
|
||||||
using Npgsql;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
namespace WorldTime;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Database abstractions
|
|
||||||
/// </summary>
|
|
||||||
public class Database {
|
|
||||||
private const string UserDatabase = "userdata";
|
|
||||||
|
|
||||||
private readonly string _connectionString;
|
|
||||||
|
|
||||||
internal Database(string connectionString) {
|
|
||||||
_connectionString = connectionString;
|
|
||||||
DoInitialDatabaseSetupAsync().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets up and opens a database connection.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<NpgsqlConnection> OpenConnectionAsync() {
|
|
||||||
var db = new NpgsqlConnection(_connectionString);
|
|
||||||
await db.OpenAsync().ConfigureAwait(false);
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DoInitialDatabaseSetupAsync() {
|
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $"create table if not exists {UserDatabase} ("
|
|
||||||
+ $"guild_id BIGINT, "
|
|
||||||
+ "user_id BIGINT, "
|
|
||||||
+ "zone TEXT NOT NULL, "
|
|
||||||
+ "last_active TIMESTAMPTZ NOT NULL DEFAULT now(), "
|
|
||||||
+ "PRIMARY KEY (guild_id, user_id)" // index automatically created with this
|
|
||||||
+ ")";
|
|
||||||
await c.ExecuteNonQueryAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if a given guild contains at least one user data entry with recent enough activity.
|
|
||||||
/// </summary>
|
|
||||||
internal async Task<bool> HasAnyAsync(SocketGuild guild) {
|
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $@"
|
|
||||||
SELECT true FROM {UserDatabase}
|
|
||||||
WHERE
|
|
||||||
guild_id = @Gid
|
|
||||||
LIMIT 1
|
|
||||||
";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
|
||||||
await c.PrepareAsync().ConfigureAwait(false);
|
|
||||||
using var r = await c.ExecuteReaderAsync().ConfigureAwait(false);
|
|
||||||
return await r.ReadAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the number of unique time zones in the database.
|
|
||||||
/// </summary>
|
|
||||||
internal async Task<int> GetDistinctZoneCountAsync() {
|
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $"SELECT COUNT(DISTINCT zone) FROM {UserDatabase}";
|
|
||||||
return (int)((long?)await c.ExecuteScalarAsync() ?? -1); // ExecuteScalarAsync returns a long here
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes the specified user from the database.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True if the removal was successful. False typically if the user did not exist.</returns>
|
|
||||||
internal async Task<bool> DeleteUserAsync(SocketGuildUser user) {
|
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $"DELETE FROM {UserDatabase} " +
|
|
||||||
"WHERE guild_id = @Gid AND user_id = @Uid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)user.Guild.Id;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)user.Id;
|
|
||||||
await c.PrepareAsync().ConfigureAwait(false);
|
|
||||||
return await c.ExecuteNonQueryAsync().ConfigureAwait(false) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Inserts/updates the specified user in the database.
|
|
||||||
/// </summary>
|
|
||||||
internal async Task UpdateUserAsync(SocketGuildUser user, string timezone) {
|
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $"INSERT INTO {UserDatabase} (guild_id, user_id, zone) " +
|
|
||||||
"VALUES (@Gid, @Uid, @Zone) " +
|
|
||||||
"ON CONFLICT (guild_id, user_id) DO " +
|
|
||||||
"UPDATE SET zone = EXCLUDED.zone";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)user.Guild.Id;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)user.Id;
|
|
||||||
c.Parameters.Add("@Zone", NpgsqlDbType.Text).Value = timezone;
|
|
||||||
await c.PrepareAsync().ConfigureAwait(false);
|
|
||||||
await c.ExecuteNonQueryAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the time zone name of a single user.
|
|
||||||
/// </summary>
|
|
||||||
internal async Task<string?> GetUserZoneAsync(SocketGuildUser user) {
|
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $"SELECT zone FROM {UserDatabase} " +
|
|
||||||
"WHERE guild_id = @Gid AND user_id = @Uid";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)user.Guild.Id;
|
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)user.Id;
|
|
||||||
await c.PrepareAsync().ConfigureAwait(false);
|
|
||||||
return (string?)await c.ExecuteScalarAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves all known user time zones for the given guild.
|
|
||||||
/// Further filtering should be handled by the consumer.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// An unsorted dictionary. Keys are time zones, values are user IDs representative of those zones.
|
|
||||||
/// </returns>
|
|
||||||
internal async Task<Dictionary<string, List<ulong>>> GetGuildZonesAsync(ulong guildId) {
|
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
|
||||||
using var c = db.CreateCommand();
|
|
||||||
c.CommandText = $@" -- Simpler query than 1.x; most filtering is now done by caller
|
|
||||||
SELECT zone, user_id FROM {UserDatabase}
|
|
||||||
WHERE
|
|
||||||
guild_id = @Gid
|
|
||||||
ORDER BY RANDOM() -- Randomize results for display purposes";
|
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
|
||||||
await c.PrepareAsync().ConfigureAwait(false);
|
|
||||||
var r = await c.ExecuteReaderAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
var resultSet = new Dictionary<string, List<ulong>>();
|
|
||||||
while (await r.ReadAsync().ConfigureAwait(false)) {
|
|
||||||
var tz = r.GetString(0);
|
|
||||||
var user = (ulong)r.GetInt64(1);
|
|
||||||
|
|
||||||
if (!resultSet.ContainsKey(tz)) resultSet.Add(tz, new List<ulong>());
|
|
||||||
resultSet[tz].Add(user);
|
|
||||||
}
|
|
||||||
return resultSet;
|
|
||||||
}
|
|
||||||
}
|
|
10
Program.cs
10
Program.cs
|
@ -18,16 +18,10 @@ class Program {
|
||||||
Environment.Exit((int)ExitCodes.ConfigError);
|
Environment.Exit((int)ExitCodes.ConfigError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Database? d = null;
|
Data.BotDatabaseContext.NpgsqlConnectionString = cfg.DbConnectionString;
|
||||||
try {
|
|
||||||
d = new(cfg.DbConnectionString);
|
|
||||||
} catch (Npgsql.NpgsqlException e) {
|
|
||||||
Console.WriteLine("Error when attempting to connect to database: " + e.Message);
|
|
||||||
Environment.Exit((int)ExitCodes.DatabaseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.CancelKeyPress += OnCancelKeyPressed;
|
Console.CancelKeyPress += OnCancelKeyPressed;
|
||||||
_bot = new WorldTime(cfg, d);
|
_bot = new WorldTime(cfg);
|
||||||
await _bot.StartAsync().ConfigureAwait(false);
|
await _bot.StartAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
await Task.Delay(-1).ConfigureAwait(false);
|
await Task.Delay(-1).ConfigureAwait(false);
|
||||||
|
|
20
WorldTime.cs
20
WorldTime.cs
|
@ -4,6 +4,7 @@ using Discord.Interactions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using WorldTime.Data;
|
||||||
|
|
||||||
namespace WorldTime;
|
namespace WorldTime;
|
||||||
|
|
||||||
|
@ -33,10 +34,9 @@ internal class WorldTime : IDisposable {
|
||||||
|
|
||||||
internal Configuration Config { get; }
|
internal Configuration Config { get; }
|
||||||
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
|
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
|
||||||
internal Database Database => _services.GetRequiredService<Database>();
|
|
||||||
|
|
||||||
public WorldTime(Configuration cfg, Database d) {
|
public WorldTime(Configuration cfg) {
|
||||||
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
Program.Log(nameof(WorldTime), $"Version {ver!.ToString(3)} is starting...");
|
Program.Log(nameof(WorldTime), $"Version {ver!.ToString(3)} is starting...");
|
||||||
|
|
||||||
Config = cfg;
|
Config = cfg;
|
||||||
|
@ -51,7 +51,7 @@ internal class WorldTime : IDisposable {
|
||||||
_services = new ServiceCollection()
|
_services = new ServiceCollection()
|
||||||
.AddSingleton(new DiscordShardedClient(clientConf))
|
.AddSingleton(new DiscordShardedClient(clientConf))
|
||||||
.AddSingleton(s => new InteractionService(s.GetRequiredService<DiscordShardedClient>()))
|
.AddSingleton(s => new InteractionService(s.GetRequiredService<DiscordShardedClient>()))
|
||||||
.AddSingleton(d)
|
.AddTransient(typeof(BotDatabaseContext))
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
DiscordClient.Log += DiscordClient_Log;
|
DiscordClient.Log += DiscordClient_Log;
|
||||||
DiscordClient.ShardReady += DiscordClient_ShardReady;
|
DiscordClient.ShardReady += DiscordClient_ShardReady;
|
||||||
|
@ -60,7 +60,7 @@ internal class WorldTime : IDisposable {
|
||||||
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
|
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
|
||||||
iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
|
iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
|
||||||
|
|
||||||
_commandsTxt = new CommandsText(this, Database);
|
_commandsTxt = new CommandsText(this, _services);
|
||||||
|
|
||||||
// Start status reporting thread
|
// Start status reporting thread
|
||||||
_mainCancel = new CancellationTokenSource();
|
_mainCancel = new CancellationTokenSource();
|
||||||
|
@ -189,10 +189,12 @@ internal class WorldTime : IDisposable {
|
||||||
|
|
||||||
// Proactively fill guild user cache if the bot has any data for the respective guild
|
// Proactively fill guild user cache if the bot has any data for the respective guild
|
||||||
// Can skip an extra query if the last_seen update is known to have been successful, otherwise query for any users
|
// Can skip an extra query if the last_seen update is known to have been successful, otherwise query for any users
|
||||||
var guild = channel.Guild;
|
if (!channel.Guild.HasAllMembers) {
|
||||||
if (!guild.HasAllMembers && await Database.HasAnyAsync(guild)) {
|
using var db = _services.GetRequiredService<BotDatabaseContext>();
|
||||||
// Event handler hangs if awaited normally or used with Task.Run
|
if (db.HasAnyUsers(channel.Guild)) {
|
||||||
await Task.Factory.StartNew(guild.DownloadUsersAsync);
|
// Event handler hangs if awaited normally or used with Task.Run
|
||||||
|
await Task.Factory.StartNew(channel.Guild.DownloadUsersAsync);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,18 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.5.0" />
|
<PackageReference Include="Discord.Net" Version="3.5.0" />
|
||||||
|
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</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.1.0" />
|
<PackageReference Include="NodaTime" Version="3.1.0" />
|
||||||
<PackageReference Include="Npgsql" Version="6.0.4" />
|
<PackageReference Include="Npgsql" Version="6.0.5" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Loading…
Reference in a new issue