Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using Discord.Interactions;
using Prima.DiscordNet.Attributes;
using Prima.Resources;

namespace Prima.Application.Interactions;

[ModuleScope(ModuleScopeAttribute.ModuleScoping.Global)]
public class MiscCommands : InteractionModuleBase<SocketInteractionContext>
[ModuleScope(ModuleScopeAttribute.ModuleScoping.Guild, GuildId = SpecialGuilds.CrystalExploratoryMissions)]
public class CEMMiscCommands : InteractionModuleBase<SocketInteractionContext>
{
private static readonly List<string> OopsImages = new()
{
Expand Down
5 changes: 4 additions & 1 deletion src/Prima.Application/Interactions/ForkedTowerCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
using Prima.DiscordNet;
using Prima.DiscordNet.Attributes;
using Prima.Game.FFXIV.FFLogs.Rules;
using Prima.Resources;
using Color = Discord.Color;

namespace Prima.Application.Interactions;

[Group("forked-tower", "Forked Tower run-related commands.")]
[ModuleScope(ModuleScopeAttribute.ModuleScoping.Global)]
[ModuleScope(ModuleScopeAttribute.ModuleScoping.Guild, GuildId = SpecialGuilds.CrystalExploratoryMissions)]
public class ForkedTowerCommands : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<ForkedTowerCommands> _logger;
Expand All @@ -19,6 +21,7 @@ public ForkedTowerCommands(ILogger<ForkedTowerCommands> logger)
_logger = logger;
}


[SlashCommand("prog", "Check the progress points of other members (maximum of 25).")]
public async Task ProgressPoints(
// Need to have each user be separate for d.NET (max 25 parameters)
Expand Down
81 changes: 81 additions & 0 deletions src/Prima.Application/Interactions/RoleCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Discord;
using Discord.Interactions;
using Microsoft.Extensions.Logging;
using Prima.DiscordNet.Attributes;
using Prima.Resources;
using Prima.Services;

namespace Prima.Application.Interactions;

[ModuleScope(ModuleScopeAttribute.ModuleScoping.Global)]
public class RoleCommands : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<RoleCommands> _logger;
private readonly IDbService _db;

public RoleCommands(ILogger<RoleCommands> logger, IDbService db)
{
_logger = logger;
_db = db;
}

[SlashCommand("list-vanity-roles", "Check your vanity roles on this server.")]
public async Task ListVanityRoles()
{
var guild = Context.Guild;
if (guild == null)
{
await ReplyAsync("This command can only be used in a guild.");
return;
}

_logger.LogInformation("Listing vanity roles for {User}", Context.User.Username);

var user = await Context.Client.Rest.GetGuildUserAsync(guild.Id, Context.User.Id);
var dbUser = await _db.GetUserByDiscordId(user.Id);
if (dbUser == null)
{
await RespondAsync("You are not currently registered. Please register yourself with `~iam`.",
ephemeral: true);
return;
}

// Register any untracked vanity roles
await UpdateVanityRoles(user);

// Get the vanity roles the user has, according to the guild
var roleIds = dbUser.GetVanityRoles(guild.Id);
_logger.LogInformation("Found {RoleCount} vanity roles for {User}", roleIds.Count, Context.User.Username);

var restGuild = await Context.Client.Rest.GetGuildAsync(guild.Id);
var roles = roleIds
.Select(r => restGuild.GetRole(r))
.Where(r => r != null)
.ToList();

// Return list of roles to user
await RespondAsync(
embed: new EmbedBuilder()
.WithTitle("Your vanity roles")
.WithDescription(string.Join("\n", roles.Select(r => r.Mention)))
.Build(),
ephemeral: true);
}

private async Task UpdateVanityRoles(IGuildUser user)
{
var dbUser = await _db.GetUserByDiscordId(user.Id);
if (dbUser == null)
{
return;
}

var allGuildVanityRoles = SpecialGuildVanityRoles.GetRoles(user.GuildId);
var vanityRoles = user.RoleIds.Where(roleId => allGuildVanityRoles.Contains(roleId)).ToList();

dbUser.AddVanityRoles(user.GuildId, vanityRoles);

_logger.LogInformation("Updating vanity roles for {User}", user.Username);
await _db.UpdateUser(dbUser);
}
}
5 changes: 5 additions & 0 deletions src/Prima.Tests/Mocks/MemoryDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public Task<DiscordXIVUser> GetUserByCharacterInfo(string world, string characte
return Task.FromResult(_users.FirstOrDefault(u => u.World == world && u.Name == characterName));
}

public Task<DiscordXIVUser> GetUserByDiscordId(ulong discordId)
{
return Task.FromResult(_users.FirstOrDefault(u => u.DiscordId == discordId));
}

public Task SetGlobalConfigurationProperty(string key, string value)
{
var field = typeof(GlobalConfiguration).GetField(key);
Expand Down
20 changes: 20 additions & 0 deletions src/Prima/Game/FFXIV/DiscordXIVUser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using MongoDB.Bson;
Expand Down Expand Up @@ -35,6 +36,25 @@ [BsonRequired] [BsonRepresentation(BsonType.String)]

public bool Verified;

// Keyed on guild ID (keys must be represented as strings)
public Dictionary<string, IList<ulong>> VanityRoles = new();

public IList<ulong> GetVanityRoles(ulong guildId)
{
return VanityRoles?.GetValueOrDefault(guildId.ToString(), new List<ulong>()) ?? new List<ulong>();
}

public void AddVanityRoles(ulong guildId, IEnumerable<ulong> roles)
{
VanityRoles ??= new Dictionary<string, IList<ulong>>();
if (!VanityRoles.ContainsKey(guildId.ToString())) VanityRoles.Add(guildId.ToString(), new List<ulong>());

foreach (var role in roles)
{
VanityRoles[guildId.ToString()].Add(role);
}
}

public static async Task<(DiscordXIVUser?, LodestoneCharacter?)> CreateFromLodestoneId(
LodestoneClient lodestone,
ulong lodestoneId, ulong discordId)
Expand Down
29 changes: 29 additions & 0 deletions src/Prima/Resources/SpecialGuildVanityRoles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;

namespace Prima.Resources
{
public static class SpecialGuildVanityRoles
{
public static readonly Dictionary<ulong, ulong[]> Roles = new()
{
{
SpecialGuilds.CrystalExploratoryMissions,
new ulong[]
{
/* Infamy of Blood */ 1381436271548829797,
/* Cleared Forked Tower */ 1381433968431202424,
/* Savage Queen */ 806363209040134174,
/* Cleared Delubrum (Savage) */ 806362589134454805,
/* Arsenal Master */ 583790261650456581,
/* Cleared Arsenal */ 552639779930636298,
}
},
};

public static ulong[] GetRoles(ulong guildId)
{
return Roles.TryGetValue(guildId, out var roles) ? roles : Array.Empty<ulong>();
}
}
}
2 changes: 1 addition & 1 deletion src/Prima/Resources/SpecialGuilds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public static class SpecialGuilds
{
public const ulong CrystalExploratoryMissions =
#if DEBUG
550910482194890781
PrimaShouji
#else
550702475112480769
#endif
Expand Down
55 changes: 44 additions & 11 deletions src/Prima/Services/DbService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ public DbService(ILogger<DbService> logger)
});
}

public Task<DiscordXIVUser?> GetUserByDiscordId(ulong discordId)
{
return WithLock(async () =>
{
_logger.LogInformation("Fetching user by Discord ID: {DiscordId}", discordId);
return await _users.Find(u => u.DiscordId == discordId).FirstOrDefaultAsync();
});
}

public async Task SetGlobalConfigurationProperty(string key, string value)
{
_logger.LogInformation("Setting global configuration property: {ConfigKey}={ConfigValue}", key, value);
Expand Down Expand Up @@ -363,23 +372,29 @@ public async Task RemoveGuildTextGreylistEntry(ulong guildId, string regexString
}
}

public async Task AddUser(DiscordXIVUser user)
public Task AddUser(DiscordXIVUser user)
{
_logger.LogInformation("Registering user {UserId}", user.DiscordId);
if (await (await _users.FindAsync(u => u.DiscordId == user.DiscordId)).AnyAsync().ConfigureAwait(false))
await _users.DeleteOneAsync(u => u.DiscordId == user.DiscordId);
await _users.InsertOneAsync(user);
return WithLock(async () =>
{
_logger.LogInformation("Registering user {UserId}", user.DiscordId);
if (await (await _users.FindAsync(u => u.DiscordId == user.DiscordId)).AnyAsync().ConfigureAwait(false))
await _users.DeleteOneAsync(u => u.DiscordId == user.DiscordId);
await _users.InsertOneAsync(user);
});
}

public Task UpdateUser(DiscordXIVUser user) => AddUser(user);

public async Task<bool> RemoveUser(string world, string name)
public Task<bool> RemoveUser(string world, string name)
{
_logger.LogInformation("Unregistering user with character ({World}) {CharacterName}", world, name);
var filterBuilder = Builders<DiscordXIVUser>.Filter;
var filter = filterBuilder.Eq(props => props.World, world) & filterBuilder.Eq(props => props.Name, name);
var result = await _users.DeleteOneAsync(filter);
return result.DeletedCount > 0;
return WithLock(async () =>
{
_logger.LogInformation("Unregistering user with character ({World}) {CharacterName}", world, name);
var filterBuilder = Builders<DiscordXIVUser>.Filter;
var filter = filterBuilder.Eq(props => props.World, world) & filterBuilder.Eq(props => props.Name, name);
var result = await _users.DeleteOneAsync(filter);
return result.DeletedCount > 0;
});
}

public async Task<bool> RemoveUser(ulong lodestoneId)
Expand Down Expand Up @@ -435,6 +450,24 @@ public async Task DeleteChannelDescription(ulong channelId)
}
}

private async Task WithLock(Func<Task> action)
{
if (!await _lock.WaitAsync(TimeSpan.FromSeconds(LockTimeoutSeconds)).ConfigureAwait(false))
{
_logger.LogWarning("Could not acquire database lock");
throw new TimeoutException("Could not acquire database lock");
}

try
{
await action();
}
finally
{
_lock.Release();
}
}

private async Task<T> WithLock<T>(Func<Task<T>> action)
{
if (!await _lock.WaitAsync(TimeSpan.FromSeconds(LockTimeoutSeconds)).ConfigureAwait(false))
Expand Down
2 changes: 2 additions & 0 deletions src/Prima/Services/IDbService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public interface IDbService
IAsyncEnumerable<VoteHost> VoteHosts { get; }
IAsyncEnumerable<EphemeralPin> EphemeralPins { get; }

Task<DiscordXIVUser?> GetUserByDiscordId(ulong discordId);

Task<DiscordXIVUser?> GetUserByCharacterInfo(string? world, string? characterName);

Task SetGlobalConfigurationProperty(string key, string value);
Expand Down
Loading