Merge pull request #1454 from tgstation/FixingChannelZero [TGSDeploy]

Discord Improvements (v5.10.0)
This commit is contained in:
Jordan Dominion
2023-04-14 16:14:04 -04:00
committed by GitHub
8 changed files with 415 additions and 148 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="ControlPanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>5.9.0</TgsCoreVersion>
<TgsCoreVersion>5.10.0</TgsCoreVersion>
<TgsConfigVersion>4.5.0</TgsConfigVersion>
<TgsApiVersion>9.9.0</TgsApiVersion>
<TgsApiLibraryVersion>10.3.0</TgsApiLibraryVersion>
@@ -212,15 +212,17 @@ namespace Tgstation.Server.Host.Components.Chat
.ToList();
}
var newMappings = results.Select(tuple => new ChannelMapping
{
IsWatchdogChannel = tuple.Item1.IsWatchdogChannel == true,
IsUpdatesChannel = tuple.Item1.IsUpdatesChannel == true,
IsAdminChannel = tuple.Item1.IsAdminChannel == true,
ProviderChannelId = tuple.Item2.RealId,
ProviderId = connectionId,
Channel = tuple.Item2,
});
var newMappings = results.SelectMany(
kvp => kvp.Value.Select(
channelRepresentation => new ChannelMapping
{
IsWatchdogChannel = kvp.Key.IsWatchdogChannel == true,
IsUpdatesChannel = kvp.Key.IsUpdatesChannel == true,
IsAdminChannel = kvp.Key.IsAdminChannel == true,
ProviderChannelId = channelRepresentation.RealId,
ProviderId = connectionId,
Channel = channelRepresentation,
}));
ulong baseId;
lock (synchronizationLock)
@@ -594,10 +596,11 @@ namespace Tgstation.Server.Host.Components.Chat
/// </summary>
/// <param name="provider">The <see cref="IProvider"/> who recevied <paramref name="message"/>.</param>
/// <param name="message">The <see cref="Message"/> to process. If <see langword="null"/>, this indicates the provider reconnected.</param>
/// <param name="recursed">If we are called recursively after remapping the provider.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
#pragma warning disable CA1502
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
async Task ProcessMessage(IProvider provider, Message message, bool recursed, CancellationToken cancellationToken)
#pragma warning restore CA1502
{
if (!provider.Connected)
@@ -617,6 +620,7 @@ namespace Tgstation.Server.Host.Components.Chat
var providerChannelId = message.User.Channel.RealId;
KeyValuePair<ulong, ChannelMapping>? mappedChannel;
long providerId;
bool hasChannelZero;
lock (providers)
{
// important, otherwise we could end up processing during shutdown
@@ -630,6 +634,18 @@ namespace Tgstation.Server.Host.Components.Chat
.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == providerChannelId)
.Select(x => (KeyValuePair<ulong, ChannelMapping>?)x)
.FirstOrDefault();
hasChannelZero = mappedChannels
.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == 0)
.Any();
}
if (!recursed && !mappedChannel.HasValue && hasChannelZero)
{
logger.LogInformation("Receieved message from unmapped channel whose provider contains ID 0. Remapping...");
await RemapProvider(provider, cancellationToken);
logger.LogTrace("Resume processing original message...");
await ProcessMessage(provider, message, true, cancellationToken);
return;
}
if (message.User.Channel.IsPrivateChannel)
@@ -646,11 +662,17 @@ namespace Tgstation.Server.Host.Components.Chat
newId);
mappedChannels.Add(newId, new ChannelMapping
{
IsWatchdogChannel = false,
ProviderChannelId = message.User.Channel.RealId,
ProviderId = providerId,
Channel = message.User.Channel,
});
logger.LogTrace(
"Mapping DM {connectionName}:{userId} ({userFriendlyName}) as {newId}",
message.User.Channel.ConnectionName,
message.User.RealId,
message.User.FriendlyName,
newId);
message.User.Channel.RealId = newId;
}
else
@@ -668,7 +690,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
message.User.Channel.RealId,
},
null,
message,
new MessageContent
{
Text = "TGS: Processing error, check logs!",
@@ -892,7 +914,7 @@ namespace Tgstation.Server.Host.Components.Chat
using (LogContext.PushProperty("ChatMessage", messageNumber))
try
{
await ProcessMessage(completedMessageTaskKvp.Key, message, cancellationToken);
await ProcessMessage(completedMessageTaskKvp.Key, message, false, cancellationToken);
}
catch (Exception ex)
{
@@ -13,14 +13,17 @@ using Remora.Discord.API.Abstractions.Gateway.Commands;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Abstractions.Results;
using Remora.Discord.API.Objects;
using Remora.Discord.Gateway;
using Remora.Discord.Gateway.Extensions;
using Remora.Rest.Core;
using Remora.Rest.Results;
using Remora.Results;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
@@ -47,6 +50,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
}
/// <summary>
/// The <see cref="ChannelType"/>s supported by the <see cref="DiscordProvider"/> for mapping.
/// </summary>
static readonly ChannelType[] SupportedGuildChannelTypes = new[]
{
ChannelType.GuildText,
ChannelType.GuildAnnouncement,
ChannelType.PrivateThread,
ChannelType.PublicThread,
};
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="DiscordProvider"/>.
/// </summary>
@@ -102,11 +116,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
Snowflake currentUserId;
/// <summary>
/// The bot's username at the time of connection.
/// </summary>
string initialUserName;
/// <summary>
/// If <see cref="serviceProvider"/> is being disposed.
/// </summary>
@@ -253,46 +262,36 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (!result.IsSuccess)
Logger.LogWarning(
"Failed to send to channel {channelId}: {error}",
"Failed to send to channel {channelId}: {result}",
channelId,
result.Error);
result.LogFormat());
}
try
{
if (channelId == 0)
{
var usersClient = serviceProvider.GetRequiredService<IDiscordRestUserAPI>();
var currentGuildsResponse = await usersClient.GetCurrentUserGuildsAsync(ct: cancellationToken);
if (!currentGuildsResponse.IsSuccess)
{
Logger.LogWarning(
"Error retrieving current discord guilds: {error}",
currentGuildsResponse.Error.Message);
return;
}
var guildsClient = serviceProvider.GetRequiredService<IDiscordRestGuildAPI>();
var guildsChannelsTasks = currentGuildsResponse.Entity.Select(
guild => guildsClient.GetGuildChannelsAsync(guild.ID.Value, cancellationToken));
await Task.WhenAll(guildsChannelsTasks);
var unmappedTextChannels = guildsChannelsTasks
.Select(task => task.Result)
.SelectMany(guildChannels => guildChannels.Entity)
.Where(guildChannel => guildChannel.Type == ChannelType.GuildText);
IEnumerable<IChannel> unmappedTextChannels;
var allAccessibleTextChannels = await GetAllAccessibleTextChannels(cancellationToken);
lock (mappedChannels)
unmappedTextChannels = unmappedTextChannels
{
unmappedTextChannels = allAccessibleTextChannels
.Where(x => !mappedChannels.Contains(x.ID.Value))
.ToList();
var remapRequired = unmappedTextChannels.Any()
|| mappedChannels.Any(
mappedChannel => !allAccessibleTextChannels.Any(
accessibleTextChannel => accessibleTextChannel.ID == new Snowflake(mappedChannel)));
if (remapRequired)
EnqueueMessage(null);
}
// discord API confirmed weak boned: https://stackoverflow.com/a/52462336
if (unmappedTextChannels.Any())
{
Logger.LogTrace("Dispatching to {0} unmapped channels...", unmappedTextChannels.Count());
Logger.LogDebug("Dispatching to {count} unmapped channels...", unmappedTextChannels.Count());
await Task.WhenAll(
unmappedTextChannels.Select(
x => SendToChannel(x.ID)));
@@ -342,7 +341,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Timestamp = estimatedCompletionTime ?? default,
};
Logger.LogTrace("Attempting to post deploy embed to channel {0}...", channelId);
Logger.LogTrace("Attempting to post deploy embed to channel {channelId}...", channelId);
var channelsClient = serviceProvider.GetRequiredService<IDiscordRestChannelAPI>();
var messageResponse = await channelsClient.CreateMessageAsync(
@@ -353,7 +352,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
;
if (!messageResponse.IsSuccess)
Logger.LogWarning("Failed to post deploy embed to channel {0}: {1}", channelId, messageResponse.Error.Message);
Logger.LogWarning("Failed to post deploy embed to channel {channelId}: {result}", channelId, messageResponse.LogFormat());
return async (errorMessage, dreamMakerOutput) =>
{
@@ -412,8 +411,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (!createUpdatedMessageResponse.IsSuccess)
Logger.LogWarning(
"Creating updated deploy embed failed! Error: {0}",
createUpdatedMessageResponse.Error.Message);
"Creating updated deploy embed failed: {result}",
createUpdatedMessageResponse.LogFormat());
}
if (!messageResponse.IsSuccess)
@@ -431,9 +430,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (!editResponse.IsSuccess)
{
Logger.LogWarning(
"Updating deploy embed {0} failed, attempting new post! Error: {1}",
"Updating deploy embed {messageId} failed, attempting new post: {result}",
messageResponse.Entity.ID,
editResponse.Error.Message);
editResponse.LogFormat());
await CreateUpdatedMessage();
}
}
@@ -481,7 +480,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (!channelResponse.IsSuccess)
{
Logger.LogWarning(
"Failed to get channel {0} in response to message {1}!",
"Failed to get channel {channelId} in response to message {messageId}!",
messageCreateEvent.ChannelID,
messageCreateEvent.ID);
@@ -503,7 +502,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
if (mentionedUs)
Logger.LogTrace(
"Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!",
"Ignoring mention from {channelId} ({channelName}) by {authorId} ({authorName}). Channel not mapped!",
messageCreateEvent.ChannelID,
channelResponse.Entity.Name,
messageCreateEvent.Author.ID,
@@ -521,9 +520,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
guildName = messageGuildResponse.Entity.Name;
else
Logger.LogWarning(
"Failed to get channel {0} in response to message {1}!",
"Failed to get channel {channelID} in response to message {messageID}: {result}",
messageCreateEvent.ChannelID,
messageCreateEvent.ID);
messageCreateEvent.ID,
messageGuildResponse.LogFormat());
}
var result = new DiscordMessage
@@ -600,12 +600,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
var currentUserResult = await userClient.GetCurrentUserAsync(localCombinedCts.Token);
if (!currentUserResult.IsSuccess)
{
Logger.LogWarning("Unable to retrieve current user: {0}", currentUserResult.Error.Message);
Logger.LogWarning("Unable to retrieve current user: {result}", currentUserResult.LogFormat());
throw new JobException(ErrorCode.ChatCannotConnectProvider);
}
currentUserId = currentUserResult.Entity.ID;
initialUserName = currentUserResult.Entity.Username;
}
finally
{
@@ -639,97 +638,139 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
localGatewayCts.Cancel();
var gatewayResult = await localGatewayTask;
if (!gatewayResult.IsSuccess)
Logger.LogWarning("Gateway issue: {0}", gatewayResult.Error.Message);
Logger.LogWarning("Gateway issue: {result}", gatewayResult.LogFormat());
localGatewayCts.Dispose();
}
/// <inheritdoc />
protected override async Task<IReadOnlyCollection<Tuple<Models.ChatChannel, ChannelRepresentation>>> MapChannelsImpl(IEnumerable<Models.ChatChannel> channels, CancellationToken cancellationToken)
protected override async Task<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> MapChannelsImpl(IEnumerable<Models.ChatChannel> channels, CancellationToken cancellationToken)
{
if (channels == null)
throw new ArgumentNullException(nameof(channels));
var remapRequired = false;
var guildsClient = serviceProvider.GetRequiredService<IDiscordRestGuildAPI>();
async Task<Tuple<Models.ChatChannel, ChannelRepresentation>> GetModelChannelFromDBChannel(Models.ChatChannel channelFromDB)
async Task<Tuple<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> GetModelChannelFromDBChannel(Models.ChatChannel channelFromDB)
{
if (!channelFromDB.DiscordChannelId.HasValue)
throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
var channelId = channelFromDB.DiscordChannelId.Value;
string connectionName;
string friendlyName;
if (channelId == 0)
var channelsClient = serviceProvider.GetRequiredService<IDiscordRestChannelAPI>();
var discordChannelResponse = await channelsClient.GetChannelAsync(new Snowflake(channelId), cancellationToken);
if (!discordChannelResponse.IsSuccess)
{
connectionName = initialUserName;
friendlyName = "(Unmapped accessible channels)";
Logger.LogWarning(
"Error retrieving discord channel {channelId}: {result}",
channelId,
discordChannelResponse.LogFormat());
remapRequired |= !(discordChannelResponse.Error is RestResultError<RestError> restResultError
&& (restResultError.Error?.Code == DiscordError.MissingAccess
|| restResultError.Error?.Code == DiscordError.UnknownChannel));
return null;
}
else
var channelType = discordChannelResponse.Entity.Type;
if (!SupportedGuildChannelTypes.Contains(channelType))
{
var channelsClient = serviceProvider.GetRequiredService<IDiscordRestChannelAPI>();
var discordChannelResponse = await channelsClient.GetChannelAsync(new Snowflake(channelId), cancellationToken);
if (!discordChannelResponse.IsSuccess)
{
Logger.LogWarning(
"Error retrieving discord channel {channelId}: {error} Inner: {innerError}",
channelId,
discordChannelResponse.Error.Message,
discordChannelResponse.Inner?.Error?.Message);
remapRequired = true;
return null;
}
Logger.LogWarning("Cound not map channel {channelId}! Incorrect type: {channelType}", channelId, discordChannelResponse.Entity.Type);
return null;
}
var channelType = discordChannelResponse.Entity.Type;
if (channelType != ChannelType.GuildText && channelType != ChannelType.GuildAnnouncement)
{
Logger.LogWarning("Cound not map channel {channelId}! Incorrect type: {channelType}", channelId, discordChannelResponse.Entity.Type);
return null;
}
var guildId = discordChannelResponse.Entity.GuildID.Value;
friendlyName = discordChannelResponse.Entity.Name.Value;
var guildId = discordChannelResponse.Entity.GuildID.Value;
var guildsClient = serviceProvider.GetRequiredService<IDiscordRestGuildAPI>();
var guildsResponse = await guildsClient.GetGuildAsync(
var guildsResponse = await guildsClient.GetGuildAsync(
guildId,
false,
cancellationToken);
if (!guildsResponse.IsSuccess)
{
Logger.LogWarning(
"Error retrieving discord guild {guildID}: {result}",
guildId,
false,
cancellationToken);
if (!guildsResponse.IsSuccess)
{
Logger.LogWarning(
"Error retrieving discord guild {guildID}: {error} Inner: {innerError}",
guildId,
guildsResponse.Error.Message,
guildsResponse.Inner?.Error?.Message);
remapRequired = true;
return null;
}
connectionName = guildsResponse.Entity.Name;
guildsResponse.LogFormat());
remapRequired |= true;
return null;
}
var connectionName = guildsResponse.Entity.Name;
var channelModel = new ChannelRepresentation
{
RealId = channelId,
IsAdminChannel = channelFromDB.IsAdminChannel == true,
ConnectionName = connectionName,
FriendlyName = friendlyName,
ConnectionName = guildsResponse.Entity.Name,
FriendlyName = discordChannelResponse.Entity.Name.Value,
IsPrivateChannel = false,
Tag = channelFromDB.Tag,
EmbedsSupported = true,
};
Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName);
return Tuple.Create(channelFromDB, channelModel);
Logger.LogTrace("Mapped channel {realId}: {friendlyName}", channelModel.RealId, channelModel.FriendlyName);
return Tuple.Create<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
channelFromDB,
new List<ChannelRepresentation> { channelModel });
}
var tasks = channels
.Select(x => GetModelChannelFromDBChannel(x))
.Where(x => x.DiscordChannelId != 0)
.Select(GetModelChannelFromDBChannel)
.ToList();
await Task.WhenAll(tasks);
var channelIdZeroModel = channels.FirstOrDefault(x => x.DiscordChannelId == 0);
if (channelIdZeroModel != null)
{
Logger.LogInformation("Mapping ALL additional accessible text channels");
var allAccessibleChannels = await GetAllAccessibleTextChannels(cancellationToken);
var unmappedTextChannels = allAccessibleChannels
.Where(x => !tasks.Any(task => task.Result != null && new Snowflake(task.Result.Item1.DiscordChannelId.Value) == x.ID));
async Task<Tuple<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> CreateMappingsForUnmappedChannels()
{
var unmappedTasks =
unmappedTextChannels.Select(
async unmappedTextChannel =>
{
var fakeChannelModel = new Models.ChatChannel
{
DiscordChannelId = unmappedTextChannel.ID.Value,
IsAdminChannel = channelIdZeroModel.IsAdminChannel,
Tag = channelIdZeroModel.Tag,
};
var tuple = await GetModelChannelFromDBChannel(fakeChannelModel);
return tuple?.Item2.First();
})
.ToList();
// Add catch-all channel
unmappedTasks.Add(Task.FromResult(
new ChannelRepresentation
{
IsAdminChannel = channelIdZeroModel.IsAdminChannel.Value,
ConnectionName = "(Unknown Discord Guilds)",
EmbedsSupported = true,
FriendlyName = "(Unknown Discord Channels)",
RealId = 0,
Tag = channelIdZeroModel.Tag,
}));
await Task.WhenAll(unmappedTasks);
return Tuple.Create<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
channelIdZeroModel,
unmappedTasks.Select(x => x.Result).Where(x => x != null).ToList());
}
var task = CreateMappingsForUnmappedChannels();
await task;
tasks.Add(task);
}
var enumerator = tasks
.Select(x => x.Result)
.Where(x => x != null)
@@ -738,13 +779,76 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
lock (mappedChannels)
{
mappedChannels.Clear();
mappedChannels.AddRange(enumerator.Select(x => x.Item2.RealId));
mappedChannels.AddRange(enumerator.SelectMany(x => x.Item2).Select(x => x.RealId));
}
if (remapRequired)
{
Logger.LogWarning("Some channels failed to load with unknown errors. We will request that these be remapped, but it may result in communication spam. Please check prior logs and report an issue if this occurs.");
EnqueueMessage(null);
}
return enumerator;
return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(enumerator.Select(x => new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(x.Item1, x.Item2)));
}
/// <summary>
/// Get all text <see cref="IChannel"/>s accessible to and supported by the bot.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in an <see cref="IEnumerable{T}"/> of accessible and compatible <see cref="IChannel"/>s.</returns>
async Task<IEnumerable<IChannel>> GetAllAccessibleTextChannels(CancellationToken cancellationToken)
{
var usersClient = serviceProvider.GetRequiredService<IDiscordRestUserAPI>();
var currentGuildsResponse = await usersClient.GetCurrentUserGuildsAsync(ct: cancellationToken);
if (!currentGuildsResponse.IsSuccess)
{
Logger.LogWarning(
"Error retrieving current discord guilds: {result}",
currentGuildsResponse.LogFormat());
return Enumerable.Empty<IChannel>();
}
var guildsClient = serviceProvider.GetRequiredService<IDiscordRestGuildAPI>();
async Task<IEnumerable<IChannel>> GetGuildChannels(IPartialGuild guild)
{
var channelsTask = guildsClient.GetGuildChannelsAsync(guild.ID.Value, cancellationToken);
var threads = await guildsClient.ListActiveGuildThreadsAsync(guild.ID.Value, cancellationToken);
if (!threads.IsSuccess)
Logger.LogWarning(
"Error retrieving discord guild threads {guildId} ({guildName}): {result}",
guild.ID,
guild.Name,
threads.LogFormat());
var channels = await channelsTask;
if (!channels.IsSuccess)
Logger.LogWarning(
"Error retrieving discord guild channels {guildId} ({guildName}): {result}",
guild.ID,
guild.Name,
channels.LogFormat());
if (!channels.IsSuccess && !threads.IsSuccess)
return Enumerable.Empty<IChannel>();
if (channels.IsSuccess && threads.IsSuccess)
return channels.Entity.Concat(threads.Entity.Threads ?? Enumerable.Empty<IChannel>());
return channels.Entity ?? threads.Entity?.Threads ?? Enumerable.Empty<IChannel>();
}
var guildsChannelsTasks = currentGuildsResponse.Entity
.Select(GetGuildChannels)
.ToList();
await Task.WhenAll(guildsChannelsTasks);
var allAccessibleChannels = guildsChannelsTasks
.SelectMany(task => task.Result)
.Where(guildChannel => SupportedGuildChannelTypes.Contains(guildChannel.Type));
return allAccessibleChannels;
}
/// <summary>
@@ -753,7 +857,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <param name="embed">The <see cref="ChatEmbed"/> to convert.</param>
/// <returns>The parameter for sending a single <see cref="IEmbed"/>.</returns>
#pragma warning disable CA1502
private Optional<IReadOnlyList<IEmbed>> ConvertEmbed(ChatEmbed embed)
Optional<IReadOnlyList<IEmbed>> ConvertEmbed(ChatEmbed embed)
{
if (embed == null)
return default;
@@ -58,8 +58,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
/// <param name="channels">The <see cref="Api.Models.ChatChannel"/>s to map.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyCollection{T}"/> of the <see cref="ChatChannel"/>'s <see cref="ChannelRepresentation"/>s representing <paramref name="channels"/>.</returns>
Task<IReadOnlyCollection<Tuple<ChatChannel, ChannelRepresentation>>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of the <see cref="ChatChannel"/>'s <see cref="ChannelRepresentation"/>s representing <paramref name="channels"/>.</returns>
Task<Dictionary<ChatChannel, IEnumerable<ChannelRepresentation>>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken);
/// <summary>
/// Send a message to the <see cref="IProvider"/>.
@@ -267,7 +267,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
protected override Task<IReadOnlyCollection<Tuple<Models.ChatChannel, ChannelRepresentation>>> MapChannelsImpl(
protected override Task<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> MapChannelsImpl(
IEnumerable<Models.ChatChannel> channels,
CancellationToken cancellationToken)
=> Task.Factory.StartNew(
@@ -300,37 +300,40 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
else
client.RfcJoin(channelToJoin);
return (IReadOnlyCollection<Tuple<Models.ChatChannel, ChannelRepresentation>>)channels
.Select(dbChannel =>
{
var channelName = dbChannel.GetIrcChannelName();
ulong? id = null;
if (!channelIdMap.Any(y =>
return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
channels
.Select(dbChannel =>
{
if (y.Value != channelName)
return false;
id = y.Key;
return true;
}))
{
id = channelIdCounter++;
channelIdMap.Add(id.Value, channelName);
}
return Tuple.Create(
dbChannel,
new ChannelRepresentation
var channelName = dbChannel.GetIrcChannelName();
ulong? id = null;
if (!channelIdMap.Any(y =>
{
RealId = id.Value,
IsAdminChannel = dbChannel.IsAdminChannel == true,
ConnectionName = address,
FriendlyName = channelIdMap[id.Value],
IsPrivateChannel = false,
Tag = dbChannel.Tag,
EmbedsSupported = false,
});
})
.ToList();
if (y.Value != channelName)
return false;
id = y.Key;
return true;
}))
{
id = channelIdCounter++;
channelIdMap.Add(id.Value, channelName);
}
return new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
dbChannel,
new List<ChannelRepresentation>
{
new ChannelRepresentation
{
RealId = id.Value,
IsAdminChannel = dbChannel.IsAdminChannel == true,
ConnectionName = address,
FriendlyName = channelIdMap[id.Value],
IsPrivateChannel = false,
Tag = dbChannel.Tag,
EmbedsSupported = false,
},
});
}));
}
},
cancellationToken,
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
public void InitialMappingComplete() => initialConnectionTcs.TrySetResult();
/// <inheritdoc />
public async Task<IReadOnlyCollection<Tuple<ChatChannel, ChannelRepresentation>>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
public async Task<Dictionary<ChatChannel, IEnumerable<ChannelRepresentation>>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
{
try
{
@@ -198,15 +198,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
/// <param name="channels">The <see cref="ChatChannel"/>s to map.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyCollection{T}"/> of the <see cref="ChatChannel"/>'s <see cref="ChannelRepresentation"/>s representing <paramref name="channels"/>.</returns>
protected abstract Task<IReadOnlyCollection<Tuple<ChatChannel, ChannelRepresentation>>> MapChannelsImpl(
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of the <see cref="ChatChannel"/>'s <see cref="ChannelRepresentation"/>s representing <paramref name="channels"/>.</returns>
protected abstract Task<Dictionary<ChatChannel, IEnumerable<ChannelRepresentation>>> MapChannelsImpl(
IEnumerable<ChatChannel> channels,
CancellationToken cancellationToken);
/// <summary>
/// Queues a <paramref name="message"/> for <see cref="NextMessage(CancellationToken)"/>.
/// </summary>
/// <param name="message">The <see cref="Message"/> to queue.</param>
/// <param name="message">The <see cref="Message"/> to queue. A value of <see langword="null"/> indicates the channel mappings a out of date.</param>
protected void EnqueueMessage(Message message)
{
if (message == null)
@@ -686,7 +686,7 @@ namespace Tgstation.Server.Host.Controllers
/// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="Models.Instance"/> or <see langword="null"/> if ports could not be allocated.</returns>
async Task<Models.Instance> CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken)
{
var ddPort = await portAllocator.GetAvailablePort(1, false, cancellationToken);
var ddPort = await portAllocator.GetAvailablePort(1024, false, cancellationToken);
if (!ddPort.HasValue)
return null;
@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Text;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
using Remora.Rest.Results;
using Remora.Results;
namespace Tgstation.Server.Host.Extensions
{
/// <summary>
/// Extensions for <see cref="IResult"/>.
/// </summary>
static class ResultExtensions
{
/// <summary>
/// Converts a given <paramref name="result"/> into a log entry <see cref="string"/>.
/// </summary>
/// <param name="result">The <see cref="IResult"/> to convert.</param>
/// <param name="level">Used internally for nesting.</param>
/// <returns>The <see cref="string"/> formatted <paramref name="result"/>.</returns>
public static string LogFormat(this IResult result, uint level = 0)
{
if (result == null)
throw new ArgumentNullException(nameof(result));
if (result.IsSuccess)
return "SUCCESS?";
var stringBuilder = new StringBuilder();
if (result.Error != null)
{
stringBuilder.Append(result.Error.Message);
if (result.Error is RestResultError<RestError> restError)
{
stringBuilder.Append(" (");
if (restError.Error != null)
{
stringBuilder.Append(restError.Error.Code);
stringBuilder.Append(": ");
stringBuilder.Append(restError.Error.Message);
stringBuilder.Append('|');
}
stringBuilder.Append(restError.Message);
if ((restError.Error?.Errors.HasValue ?? false) && restError.Error.Errors.Value.Count > 0)
{
stringBuilder.Append(" (");
foreach (var error in restError.Error.Errors.Value)
{
stringBuilder.Append(error.Key);
stringBuilder.Append(':');
if (error.Value.IsT0)
{
FormatErrorDetails(error.Value.AsT0, stringBuilder);
}
else
FormatErrorDetails(error.Value.AsT1, stringBuilder);
stringBuilder.Append(',');
}
stringBuilder.Remove(stringBuilder.Length - 1, 1);
}
stringBuilder.Append(')');
}
}
if (result.Inner != null)
{
stringBuilder.Append(Environment.NewLine);
++level;
for (var i = 0; i < level; ++i)
stringBuilder.Append('\t');
stringBuilder.Append(result.Inner.LogFormat(level));
}
return stringBuilder.ToString();
}
/// <summary>
/// Formats given <paramref name="propertyErrorDetails"/> into a given <paramref name="stringBuilder"/>.
/// </summary>
/// <param name="propertyErrorDetails">The <see cref="IPropertyErrorDetails"/>.</param>
/// <param name="stringBuilder">The <see cref="StringBuilder"/> to mutate.</param>
static void FormatErrorDetails(IPropertyErrorDetails propertyErrorDetails, StringBuilder stringBuilder)
{
if (propertyErrorDetails == null)
return;
FormatErrorDetails(propertyErrorDetails.Errors, stringBuilder);
if (propertyErrorDetails.Errors != null && propertyErrorDetails.MemberErrors != null)
{
stringBuilder.Append(',');
}
if (propertyErrorDetails.MemberErrors != null)
{
stringBuilder.Append('{');
foreach (var error in propertyErrorDetails.MemberErrors)
{
stringBuilder.Append(error.Key);
stringBuilder.Append(':');
FormatErrorDetails(error.Value, stringBuilder);
stringBuilder.Append(',');
}
stringBuilder.Remove(stringBuilder.Length - 1, 1);
stringBuilder.Append('}');
}
}
/// <summary>
/// Formats given <paramref name="errorDetails"/> into a given <paramref name="stringBuilder"/>.
/// </summary>
/// <param name="errorDetails">The <see cref="IEnumerable{T}"/> of <see cref="IErrorDetails"/>.</param>
/// <param name="stringBuilder">The <see cref="StringBuilder"/> to mutate.</param>
static void FormatErrorDetails(IEnumerable<IErrorDetails> errorDetails, StringBuilder stringBuilder)
{
if (errorDetails == null)
return;
stringBuilder.Append('[');
foreach (var error in errorDetails)
{
stringBuilder.Append(error.Code);
stringBuilder.Append(':');
stringBuilder.Append(error.Message);
stringBuilder.Append(',');
}
stringBuilder.Remove(stringBuilder.Length - 1, 1);
stringBuilder.Append(']');
}
}
}