|
|
|
@@ -1,12 +1,20 @@
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Drawing;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
using Discord;
|
|
|
|
|
using Discord.WebSocket;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
|
|
|
|
using Remora.Discord.API.Abstractions.Objects;
|
|
|
|
|
using Remora.Discord.API.Abstractions.Rest;
|
|
|
|
|
using Remora.Discord.API.Objects;
|
|
|
|
|
using Remora.Discord.Core;
|
|
|
|
|
using Remora.Discord.Gateway;
|
|
|
|
|
using Remora.Discord.Gateway.Extensions;
|
|
|
|
|
using Remora.Results;
|
|
|
|
|
|
|
|
|
|
using Tgstation.Server.Api.Models;
|
|
|
|
|
using Tgstation.Server.Host.Jobs;
|
|
|
|
@@ -18,10 +26,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// <see cref="IProvider"/> for the Discord app.
|
|
|
|
|
/// </summary>
|
|
|
|
|
sealed class DiscordProvider : Provider
|
|
|
|
|
sealed class DiscordProvider : Provider, IDiscordResponders
|
|
|
|
|
{
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public override bool Connected => client.ConnectionState != ConnectionState.Disconnected;
|
|
|
|
|
public override bool Connected => gatewayTask?.IsCompleted == false;
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public override string BotMention
|
|
|
|
@@ -30,7 +38,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
{
|
|
|
|
|
if (!Connected)
|
|
|
|
|
throw new InvalidOperationException("Provider not connected");
|
|
|
|
|
return NormalizeMentions(client.CurrentUser.Mention);
|
|
|
|
|
return NormalizeMentions($"<@{currentUserId}>");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -40,20 +48,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
readonly IAssemblyInformationProvider assemblyInformationProvider;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The <see cref="DiscordSocketClient"/> for the <see cref="DiscordProvider"/>.
|
|
|
|
|
/// The <see cref="ServiceProvider"/> containing Discord services.
|
|
|
|
|
/// </summary>
|
|
|
|
|
readonly DiscordSocketClient client;
|
|
|
|
|
readonly ServiceProvider serviceProvider;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// <see cref="List{T}"/> of mapped <see cref="ITextChannel"/> <see cref="IEntity{TId}.Id"/>s.
|
|
|
|
|
/// <see cref="List{T}"/> of mapped channel <see cref="Snowflake"/>s.
|
|
|
|
|
/// </summary>
|
|
|
|
|
readonly List<ulong> mappedChannels;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The Discord bot token.
|
|
|
|
|
/// </summary>
|
|
|
|
|
readonly string botToken;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// <see cref="bool"/> to enable based mode. Will auto reply with a youtube link to a video that says "based on the hardware that's installed in it" to anyone saying 'based on what?' case-insensitive.
|
|
|
|
|
/// </summary>
|
|
|
|
@@ -64,6 +67,21 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
/// </summary>
|
|
|
|
|
readonly DiscordDMOutputDisplayType outputDisplayType;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The <see cref="CancellationTokenSource"/> for the <see cref="gatewayTask"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
CancellationTokenSource gatewayCts;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The <see cref="Task"/> representing the lifetime of the client.
|
|
|
|
|
/// </summary>
|
|
|
|
|
Task<Result> gatewayTask;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The bot's <see cref="Snowflake"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
Snowflake currentUserId;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Normalize a discord mention string.
|
|
|
|
|
/// </summary>
|
|
|
|
@@ -72,15 +90,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
static string NormalizeMentions(string fromDiscord) => fromDiscord.Replace("<@!", "<@", StringComparison.Ordinal);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Create a <see cref="List{T}"/> of <see cref="EmbedFieldBuilder"/>s for a discord update embed.
|
|
|
|
|
/// Create a <see cref="List{T}"/> of <see cref="IEmbedField"/>s for a discord update embed.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
|
|
|
|
|
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
|
|
|
|
|
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
|
|
|
|
|
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
|
|
|
|
|
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
|
|
|
|
|
/// <returns>A new <see cref="List{T}"/> of <see cref="EmbedFieldBuilder"/>s to use.</returns>
|
|
|
|
|
static List<EmbedFieldBuilder> BuildUpdateEmbedFields(
|
|
|
|
|
/// <returns>A new <see cref="List{T}"/> of <see cref="IEmbedField"/>s to use.</returns>
|
|
|
|
|
static List<IEmbedField> BuildUpdateEmbedFields(
|
|
|
|
|
Models.RevisionInformation revisionInformation,
|
|
|
|
|
Version byondVersion,
|
|
|
|
|
string gitHubOwner,
|
|
|
|
@@ -88,39 +106,32 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
bool localCommitPushed)
|
|
|
|
|
{
|
|
|
|
|
bool gitHub = gitHubOwner != null && gitHubRepo != null;
|
|
|
|
|
var fields = new List<EmbedFieldBuilder>
|
|
|
|
|
var fields = new List<IEmbedField>
|
|
|
|
|
{
|
|
|
|
|
new EmbedFieldBuilder
|
|
|
|
|
{
|
|
|
|
|
Name = "BYOND Version",
|
|
|
|
|
Value = $"{byondVersion.Major}.{byondVersion.Minor}{(byondVersion.Build > 0 ? $".{byondVersion.Build}" : String.Empty)}",
|
|
|
|
|
IsInline = true,
|
|
|
|
|
},
|
|
|
|
|
new EmbedFieldBuilder
|
|
|
|
|
{
|
|
|
|
|
Name = "Local Commit",
|
|
|
|
|
Value = localCommitPushed && gitHub
|
|
|
|
|
new EmbedField(
|
|
|
|
|
"BYOND Version",
|
|
|
|
|
$"{byondVersion.Major}.{byondVersion.Minor}{(byondVersion.Build > 0 ? $".{byondVersion.Build}" : String.Empty)}",
|
|
|
|
|
true),
|
|
|
|
|
new EmbedField(
|
|
|
|
|
"Local Commit",
|
|
|
|
|
localCommitPushed && gitHub
|
|
|
|
|
? $"[{revisionInformation.CommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})"
|
|
|
|
|
: revisionInformation.CommitSha.Substring(0, 7),
|
|
|
|
|
IsInline = true,
|
|
|
|
|
},
|
|
|
|
|
new EmbedFieldBuilder
|
|
|
|
|
{
|
|
|
|
|
Name = "Branch Commit",
|
|
|
|
|
Value = gitHub
|
|
|
|
|
true),
|
|
|
|
|
new EmbedField(
|
|
|
|
|
"Branch Commit",
|
|
|
|
|
gitHub
|
|
|
|
|
? $"[{revisionInformation.OriginCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})"
|
|
|
|
|
: revisionInformation.OriginCommitSha.Substring(0, 7),
|
|
|
|
|
IsInline = true,
|
|
|
|
|
},
|
|
|
|
|
true),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>())
|
|
|
|
|
.Select(x => x.TestMerge)
|
|
|
|
|
.Select(x => new EmbedFieldBuilder
|
|
|
|
|
{
|
|
|
|
|
Name = $"#{x.Number}",
|
|
|
|
|
Value = $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
|
|
|
|
|
}));
|
|
|
|
|
.Select(x => new EmbedField(
|
|
|
|
|
$"#{x.Number}",
|
|
|
|
|
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
|
|
|
|
|
false)));
|
|
|
|
|
|
|
|
|
|
return fields;
|
|
|
|
|
}
|
|
|
|
@@ -141,25 +152,32 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
{
|
|
|
|
|
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
|
|
|
|
|
|
|
|
|
mappedChannels = new List<ulong>();
|
|
|
|
|
|
|
|
|
|
var csb = new DiscordConnectionStringBuilder(chatBot.ConnectionString);
|
|
|
|
|
botToken = csb.BotToken;
|
|
|
|
|
var botToken = csb.BotToken;
|
|
|
|
|
basedMeme = csb.BasedMeme;
|
|
|
|
|
outputDisplayType = csb.DMOutputDisplay;
|
|
|
|
|
|
|
|
|
|
client = new DiscordSocketClient();
|
|
|
|
|
client.MessageReceived += Client_MessageReceived;
|
|
|
|
|
mappedChannels = new List<ulong>();
|
|
|
|
|
serviceProvider = new ServiceCollection()
|
|
|
|
|
.AddDiscordGateway(serviceProvider => botToken)
|
|
|
|
|
.AddSingleton<IDiscordResponders>(serviceProvider => this)
|
|
|
|
|
.AddResponder<DiscordForwardingResponder>()
|
|
|
|
|
.BuildServiceProvider();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public override async ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
await base.DisposeAsync().ConfigureAwait(false);
|
|
|
|
|
client.Dispose();
|
|
|
|
|
await serviceProvider.DisposeAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
// this line is purely here to shutup CA2213
|
|
|
|
|
gatewayCts?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public override Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken)
|
|
|
|
|
public override async Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (channels == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(channels));
|
|
|
|
@@ -167,10 +185,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
if (!Connected)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning("Cannot map channels, provider disconnected!");
|
|
|
|
|
return Task.FromResult<IReadOnlyCollection<ChannelRepresentation>>(Array.Empty<ChannelRepresentation>());
|
|
|
|
|
return Array.Empty<ChannelRepresentation>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ChannelRepresentation GetModelChannelFromDBChannel(Api.Models.ChatChannel channelFromDB)
|
|
|
|
|
var usersClient = serviceProvider.GetRequiredService<IDiscordRestUserAPI>();
|
|
|
|
|
var currentUserResponse = await usersClient.GetCurrentUserAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (!currentUserResponse.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning("Error retrieving current Discord user: {0}", currentUserResponse.Error.Message);
|
|
|
|
|
return Array.Empty<ChannelRepresentation>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task<ChannelRepresentation> GetModelChannelFromDBChannel(Api.Models.ChatChannel channelFromDB)
|
|
|
|
|
{
|
|
|
|
|
if (!channelFromDB.DiscordChannelId.HasValue)
|
|
|
|
|
throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
|
|
|
|
@@ -181,22 +208,44 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
string friendlyName;
|
|
|
|
|
if (channelId == 0)
|
|
|
|
|
{
|
|
|
|
|
connectionName = client.CurrentUser.Username;
|
|
|
|
|
connectionName = currentUserResponse.Entity.Username;
|
|
|
|
|
friendlyName = "(Unmapped accessible channels)";
|
|
|
|
|
discordChannelId = 0;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var discordChannel = client.GetChannel(channelId);
|
|
|
|
|
if (!(discordChannel is ITextChannel textChannel))
|
|
|
|
|
var channelsClient = serviceProvider.GetRequiredService<IDiscordRestChannelAPI>();
|
|
|
|
|
var discordChannelResponse = await channelsClient.GetChannelAsync(new Snowflake(channelId), cancellationToken);
|
|
|
|
|
if (!discordChannelResponse.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType());
|
|
|
|
|
Logger.LogWarning("Error retrieving discord channel {0}: {1}", channelId, discordChannelResponse.Error.Message);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discordChannelId = textChannel.Id;
|
|
|
|
|
connectionName = textChannel.Guild.Name;
|
|
|
|
|
friendlyName = textChannel.Name;
|
|
|
|
|
if (discordChannelResponse.Entity.Type != ChannelType.GuildText)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannelResponse.Entity.Type);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discordChannelId = discordChannelResponse.Entity.ID.Value;
|
|
|
|
|
friendlyName = discordChannelResponse.Entity.Name.Value;
|
|
|
|
|
|
|
|
|
|
var guildsClient = serviceProvider.GetRequiredService<IDiscordRestGuildAPI>();
|
|
|
|
|
var guildsResponse = await guildsClient.GetGuildAsync(
|
|
|
|
|
discordChannelResponse.Entity.GuildID.Value,
|
|
|
|
|
false,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
if (!guildsResponse.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning(
|
|
|
|
|
"Error retrieving discord guild {0}: {1}",
|
|
|
|
|
discordChannelResponse.Entity.GuildID.Value,
|
|
|
|
|
discordChannelResponse.Error.Message);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectionName = guildsResponse.Entity.Name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var channelModel = new ChannelRepresentation
|
|
|
|
@@ -208,13 +257,21 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
IsPrivateChannel = false,
|
|
|
|
|
Tag = channelFromDB.Tag,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName);
|
|
|
|
|
return channelModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var enumerator = channels
|
|
|
|
|
var tasks = channels
|
|
|
|
|
.Select(x => GetModelChannelFromDBChannel(x))
|
|
|
|
|
.Where(x => x != null).ToList();
|
|
|
|
|
.Where(x => x != null)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
|
|
|
|
|
|
var enumerator = tasks
|
|
|
|
|
.Select(x => x.Result)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
lock (mappedChannels)
|
|
|
|
|
{
|
|
|
|
@@ -222,57 +279,64 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
mappedChannels.AddRange(enumerator.Select(x => x.RealId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.FromResult<IReadOnlyCollection<ChannelRepresentation>>(enumerator);
|
|
|
|
|
return enumerator;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var requestOptions = new RequestOptions
|
|
|
|
|
var channelsClient = serviceProvider.GetRequiredService<IDiscordRestChannelAPI>();
|
|
|
|
|
async Task SendToChannel(Snowflake channelId)
|
|
|
|
|
{
|
|
|
|
|
CancelToken = cancellationToken,
|
|
|
|
|
Timeout = 10000, // prevent stupid long hold ups from this
|
|
|
|
|
};
|
|
|
|
|
var result = await channelsClient.CreateMessageAsync(
|
|
|
|
|
channelId,
|
|
|
|
|
message,
|
|
|
|
|
ct: cancellationToken);
|
|
|
|
|
|
|
|
|
|
Task SendToChannel(IMessageChannel channel) => channel.SendMessageAsync(
|
|
|
|
|
message,
|
|
|
|
|
false,
|
|
|
|
|
null,
|
|
|
|
|
requestOptions);
|
|
|
|
|
if (!result.IsSuccess)
|
|
|
|
|
Logger.LogWarning(
|
|
|
|
|
"Failed to send to channel {0}: {1}",
|
|
|
|
|
channelId,
|
|
|
|
|
result.Error.Message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (channelId == 0)
|
|
|
|
|
{
|
|
|
|
|
var unmappedTextChannels = client
|
|
|
|
|
.Guilds
|
|
|
|
|
.SelectMany(x => x.TextChannels);
|
|
|
|
|
var usersClient = serviceProvider.GetRequiredService<IDiscordRestUserAPI>();
|
|
|
|
|
var currentGuildsResponse = await usersClient.GetCurrentUserGuildsAsync(ct: cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (!currentGuildsResponse.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning(
|
|
|
|
|
"Error retrieving current discord guilds: {0}",
|
|
|
|
|
currentGuildsResponse.Error.Message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var unmappedTextChannels = currentGuildsResponse
|
|
|
|
|
.Entity
|
|
|
|
|
.SelectMany(x => x.Channels.Value);
|
|
|
|
|
|
|
|
|
|
lock (mappedChannels)
|
|
|
|
|
unmappedTextChannels = unmappedTextChannels.Where(x => !mappedChannels.Contains(x.Id));
|
|
|
|
|
unmappedTextChannels = unmappedTextChannels
|
|
|
|
|
.Where(x => !mappedChannels.Contains(x.ID.Value))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
// discord API confirmed weak boned: https://stackoverflow.com/a/52462336
|
|
|
|
|
var channelCount = 0UL;
|
|
|
|
|
var tasks = unmappedTextChannels
|
|
|
|
|
.Select(x =>
|
|
|
|
|
{
|
|
|
|
|
++channelCount;
|
|
|
|
|
return SendToChannel(x);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (channelCount > 0)
|
|
|
|
|
if (unmappedTextChannels.Any())
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("Dispatched to {0} unmapped channels...", channelCount);
|
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
|
|
|
Logger.LogTrace("Dispatching to {0} unmapped channels...", unmappedTextChannels.Count());
|
|
|
|
|
await Task.WhenAll(
|
|
|
|
|
unmappedTextChannels.Select(
|
|
|
|
|
x => SendToChannel(x.ID)))
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!(client.GetChannel(channelId) is IMessageChannel channel))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await SendToChannel(channel).ConfigureAwait(false);
|
|
|
|
|
await SendToChannel(new Snowflake(channelId)).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
@@ -283,6 +347,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
#pragma warning disable CA1506
|
|
|
|
|
public override async Task<Func<string, string, Task>> SendUpdateMessage(
|
|
|
|
|
Models.RevisionInformation revisionInformation,
|
|
|
|
|
Version byondVersion,
|
|
|
|
@@ -296,51 +361,53 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha;
|
|
|
|
|
|
|
|
|
|
var fields = BuildUpdateEmbedFields(revisionInformation, byondVersion, gitHubOwner, gitHubRepo, localCommitPushed);
|
|
|
|
|
var builder = new EmbedBuilder
|
|
|
|
|
var embed = new Embed
|
|
|
|
|
{
|
|
|
|
|
Author = new EmbedAuthorBuilder
|
|
|
|
|
Author = new EmbedAuthor
|
|
|
|
|
{
|
|
|
|
|
Name = assemblyInformationProvider.VersionPrefix,
|
|
|
|
|
Url = "https://github.com/tgstation/tgstation-server",
|
|
|
|
|
IconUrl = "https://avatars0.githubusercontent.com/u/1363778?s=280&v=4",
|
|
|
|
|
},
|
|
|
|
|
Color = Color.Gold,
|
|
|
|
|
Colour = Color.FromArgb(0xF1, 0xC4, 0x0F),
|
|
|
|
|
Description = "TGS has begun deploying active repository code to production.",
|
|
|
|
|
Fields = fields,
|
|
|
|
|
Title = "Code Deployment",
|
|
|
|
|
Footer = new EmbedFooterBuilder
|
|
|
|
|
{
|
|
|
|
|
Text = $"In progress...{(estimatedCompletionTime.HasValue ? " ETA" : String.Empty)}",
|
|
|
|
|
},
|
|
|
|
|
Timestamp = estimatedCompletionTime,
|
|
|
|
|
Footer = new EmbedFooter(
|
|
|
|
|
$"In progress...{(estimatedCompletionTime.HasValue ? " ETA" : String.Empty)}"),
|
|
|
|
|
Timestamp = estimatedCompletionTime ?? default,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Logger.LogTrace("Attempting to post deploy embed to channel {0}...", channelId);
|
|
|
|
|
if (!(client.GetChannel(channelId) is IMessageChannel channel))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("Channel ID {0} does not exist or is not an IMessageChannel!", channelId);
|
|
|
|
|
return (errorMessage, dreamMakerOutput) => Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
var channelsClient = serviceProvider.GetRequiredService<IDiscordRestChannelAPI>();
|
|
|
|
|
|
|
|
|
|
var message = await channel.SendMessageAsync(
|
|
|
|
|
var messageResponse = await channelsClient.CreateMessageAsync(
|
|
|
|
|
new Snowflake(channelId),
|
|
|
|
|
"DM: Deployment in Progress...",
|
|
|
|
|
false,
|
|
|
|
|
builder.Build(),
|
|
|
|
|
new RequestOptions
|
|
|
|
|
{
|
|
|
|
|
CancelToken = cancellationToken,
|
|
|
|
|
})
|
|
|
|
|
embeds: new List<IEmbed> { embed },
|
|
|
|
|
ct: cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (!messageResponse.IsSuccess)
|
|
|
|
|
Logger.LogWarning("Failed to post deploy embed to channel {0}: {1}", channelId, messageResponse.Error.Message);
|
|
|
|
|
|
|
|
|
|
return async (errorMessage, dreamMakerOutput) =>
|
|
|
|
|
{
|
|
|
|
|
var completionString = errorMessage == null ? "Succeeded" : "Failed";
|
|
|
|
|
builder.Footer.Text = completionString;
|
|
|
|
|
builder.Color = errorMessage == null ? Color.Green : Color.Red;
|
|
|
|
|
builder.Timestamp = DateTimeOffset.UtcNow;
|
|
|
|
|
builder.Description = errorMessage == null
|
|
|
|
|
|
|
|
|
|
embed = new Embed
|
|
|
|
|
{
|
|
|
|
|
Author = embed.Author,
|
|
|
|
|
Colour = errorMessage == null ? Color.Green : Color.Red,
|
|
|
|
|
Description = errorMessage == null
|
|
|
|
|
? "The deployment completed successfully and will be available at the next server reboot."
|
|
|
|
|
: "The deployment failed.";
|
|
|
|
|
: "The deployment failed.",
|
|
|
|
|
Fields = fields,
|
|
|
|
|
Title = embed.Title,
|
|
|
|
|
Footer = new EmbedFooter(
|
|
|
|
|
completionString),
|
|
|
|
|
Timestamp = DateTimeOffset.UtcNow,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var showDMOutput = outputDisplayType switch
|
|
|
|
|
{
|
|
|
|
@@ -352,83 +419,188 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
|
|
|
|
|
if (dreamMakerOutput != null)
|
|
|
|
|
{
|
|
|
|
|
showDMOutput = showDMOutput && dreamMakerOutput.Length < EmbedFieldBuilder.MaxFieldValueLength - (6 + Environment.NewLine.Length);
|
|
|
|
|
// https://github.com/discord-net/Discord.Net/blob/8349cd7e1eb92e9a3baff68082c30a7b43e8e9b7/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs#L431
|
|
|
|
|
const int MaxFieldValueLength = 1024;
|
|
|
|
|
showDMOutput = showDMOutput && dreamMakerOutput.Length < MaxFieldValueLength - (6 + Environment.NewLine.Length);
|
|
|
|
|
if (showDMOutput)
|
|
|
|
|
builder.AddField(new EmbedFieldBuilder
|
|
|
|
|
{
|
|
|
|
|
Name = "DreamMaker Output",
|
|
|
|
|
Value = $"```{Environment.NewLine}{dreamMakerOutput}{Environment.NewLine}```",
|
|
|
|
|
});
|
|
|
|
|
fields.Add(new EmbedField(
|
|
|
|
|
"DreamMaker Output",
|
|
|
|
|
$"```{Environment.NewLine}{dreamMakerOutput}{Environment.NewLine}```",
|
|
|
|
|
false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (errorMessage != null)
|
|
|
|
|
builder.AddField(new EmbedFieldBuilder
|
|
|
|
|
{
|
|
|
|
|
Name = "Error Message",
|
|
|
|
|
Value = errorMessage,
|
|
|
|
|
});
|
|
|
|
|
fields.Add(new EmbedField(
|
|
|
|
|
"Error Message",
|
|
|
|
|
errorMessage,
|
|
|
|
|
false));
|
|
|
|
|
|
|
|
|
|
var updatedMessage = $"DM: Deployment {completionString}!";
|
|
|
|
|
try
|
|
|
|
|
|
|
|
|
|
async Task CreateUpdatedMessage()
|
|
|
|
|
{
|
|
|
|
|
await message.ModifyAsync(
|
|
|
|
|
props =>
|
|
|
|
|
{
|
|
|
|
|
props.Content = updatedMessage;
|
|
|
|
|
props.Embed = builder.Build();
|
|
|
|
|
})
|
|
|
|
|
var createUpdatedMessageResponse = await channelsClient.CreateMessageAsync(
|
|
|
|
|
new Snowflake(channelId),
|
|
|
|
|
updatedMessage,
|
|
|
|
|
embeds: new List<IEmbed> { embed },
|
|
|
|
|
ct: cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (!createUpdatedMessageResponse.IsSuccess)
|
|
|
|
|
Logger.LogWarning(
|
|
|
|
|
"Creating updated deploy embed failed! Error: {0}",
|
|
|
|
|
createUpdatedMessageResponse.Error.Message);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
|
|
|
|
if (!messageResponse.IsSuccess)
|
|
|
|
|
await CreateUpdatedMessage();
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning(ex, "Updating deploy embed {0} failed, attempting new post!", message.Id);
|
|
|
|
|
try
|
|
|
|
|
var editResponse = await channelsClient.EditMessageAsync(
|
|
|
|
|
new Snowflake(channelId),
|
|
|
|
|
messageResponse.Entity.ID,
|
|
|
|
|
updatedMessage,
|
|
|
|
|
embeds: new List<IEmbed> { embed },
|
|
|
|
|
ct: cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (!editResponse.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
await channel.SendMessageAsync(
|
|
|
|
|
updatedMessage,
|
|
|
|
|
false,
|
|
|
|
|
builder.Build())
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex2)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning(ex2, "Posting completion deploy embed failed!");
|
|
|
|
|
Logger.LogWarning(
|
|
|
|
|
"Updating deploy embed {0} failed, attempting new post! Error: {1}",
|
|
|
|
|
messageResponse.Entity.ID,
|
|
|
|
|
editResponse.Error.Message);
|
|
|
|
|
await CreateUpdatedMessage();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
#pragma warning restore CA1506
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async Task<Result> RespondAsync(IMessageCreate messageCreateEvent, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if ((messageCreateEvent.Type != MessageType.Default
|
|
|
|
|
&& messageCreateEvent.Type != MessageType.InlineReply)
|
|
|
|
|
|| messageCreateEvent.Author.ID == currentUserId)
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
|
|
|
|
|
if (basedMeme && messageCreateEvent.Content.Equals("Based on what?", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
// DCT: None available
|
|
|
|
|
await SendMessage(
|
|
|
|
|
messageCreateEvent.ChannelID.Value,
|
|
|
|
|
"https://youtu.be/LrNu-SuFF_o",
|
|
|
|
|
default)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var channelsClient = serviceProvider.GetRequiredService<IDiscordRestChannelAPI>();
|
|
|
|
|
var channelResponse = await channelsClient.GetChannelAsync(messageCreateEvent.ChannelID, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (!channelResponse.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning(
|
|
|
|
|
"Failed to get channel {0} in response to message {1}!",
|
|
|
|
|
messageCreateEvent.ChannelID,
|
|
|
|
|
messageCreateEvent.ID);
|
|
|
|
|
|
|
|
|
|
// we'll handle the errors ourselves
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pm = channelResponse.Entity.Type == ChannelType.DM || channelResponse.Entity.Type == ChannelType.GroupDM;
|
|
|
|
|
var shouldNotAnswer = !pm;
|
|
|
|
|
if (shouldNotAnswer)
|
|
|
|
|
lock (mappedChannels)
|
|
|
|
|
shouldNotAnswer = !mappedChannels.Contains(messageCreateEvent.ChannelID.Value);
|
|
|
|
|
|
|
|
|
|
var content = NormalizeMentions(messageCreateEvent.Content);
|
|
|
|
|
var mentionedUs = messageCreateEvent.Mentions.Any(x => x.ID == currentUserId)
|
|
|
|
|
|| (!shouldNotAnswer && content.Split(' ').First().Equals(ChatManager.CommonMention, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
if (shouldNotAnswer)
|
|
|
|
|
{
|
|
|
|
|
if (mentionedUs)
|
|
|
|
|
Logger.LogTrace(
|
|
|
|
|
"Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!",
|
|
|
|
|
messageCreateEvent.ChannelID,
|
|
|
|
|
channelResponse.Entity.Name,
|
|
|
|
|
messageCreateEvent.Author.ID,
|
|
|
|
|
messageCreateEvent.Author.Username);
|
|
|
|
|
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string guildName = "UNKNOWN";
|
|
|
|
|
if (!pm)
|
|
|
|
|
{
|
|
|
|
|
var guildsClient = serviceProvider.GetRequiredService<IDiscordRestGuildAPI>();
|
|
|
|
|
var messageGuildResponse = await guildsClient.GetGuildAsync(messageCreateEvent.GuildID.Value, false, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (messageGuildResponse.IsSuccess)
|
|
|
|
|
guildName = messageGuildResponse.Entity.Name;
|
|
|
|
|
else
|
|
|
|
|
Logger.LogWarning(
|
|
|
|
|
"Failed to get channel {0} in response to message {1}!",
|
|
|
|
|
messageCreateEvent.ChannelID,
|
|
|
|
|
messageCreateEvent.ID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = new Message
|
|
|
|
|
{
|
|
|
|
|
Content = content,
|
|
|
|
|
User = new ChatUser
|
|
|
|
|
{
|
|
|
|
|
RealId = messageCreateEvent.Author.ID.Value,
|
|
|
|
|
Channel = new ChannelRepresentation
|
|
|
|
|
{
|
|
|
|
|
RealId = messageCreateEvent.ChannelID.Value,
|
|
|
|
|
IsPrivateChannel = pm,
|
|
|
|
|
ConnectionName = pm ? messageCreateEvent.Author.Username : guildName,
|
|
|
|
|
FriendlyName = channelResponse.Entity.Name.Value,
|
|
|
|
|
|
|
|
|
|
// isAdmin and Tag populated by manager
|
|
|
|
|
},
|
|
|
|
|
FriendlyName = messageCreateEvent.Author.Username,
|
|
|
|
|
Mention = NormalizeMentions($"<@{messageCreateEvent.Author.ID}>"),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
EnqueueMessage(result);
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
protected override async Task Connect(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
|
|
|
|
|
if (gatewayCts != null)
|
|
|
|
|
throw new InvalidOperationException("Discord gateway still active!");
|
|
|
|
|
|
|
|
|
|
Logger.LogTrace("Logged in.");
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
gatewayCts = new CancellationTokenSource();
|
|
|
|
|
|
|
|
|
|
var channelsAvailable = new TaskCompletionSource<object>();
|
|
|
|
|
Task ReadyCallback()
|
|
|
|
|
var gatewayCancellationToken = gatewayCts.Token;
|
|
|
|
|
var gatewayClient = serviceProvider.GetRequiredService<DiscordGatewayClient>();
|
|
|
|
|
|
|
|
|
|
// reconnects keep happening until we stop or it faults, our auto-reconnector will handle the latter
|
|
|
|
|
gatewayTask = gatewayClient.RunAsync(gatewayCancellationToken);
|
|
|
|
|
|
|
|
|
|
var userClient = serviceProvider.GetRequiredService<IDiscordRestUserAPI>();
|
|
|
|
|
|
|
|
|
|
using var localCombinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, gatewayCancellationToken);
|
|
|
|
|
var currentUserResult = await userClient.GetCurrentUserAsync(localCombinedCts.Token).ConfigureAwait(false);
|
|
|
|
|
if (!currentUserResult.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
channelsAvailable.TrySetResult(null);
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
Logger.LogWarning("Unable to retrieve current user: {0}", currentUserResult.Error.Message);
|
|
|
|
|
|
|
|
|
|
// will handle cleanup
|
|
|
|
|
await DisconnectImpl(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
throw new JobException(ErrorCode.ChatCannotConnectProvider);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client.Ready += ReadyCallback;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await client.StartAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
Logger.LogTrace("Started.");
|
|
|
|
|
|
|
|
|
|
using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
|
|
|
|
|
await channelsAvailable.Task.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
client.Ready -= ReadyCallback;
|
|
|
|
|
}
|
|
|
|
|
currentUserId = currentUserResult.Entity.ID;
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
@@ -443,153 +615,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
protected override async Task DisconnectImpl(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
var disconnectTcs = new TaskCompletionSource<object>();
|
|
|
|
|
Task DisconnectCallback(Exception exception)
|
|
|
|
|
{
|
|
|
|
|
if (exception != null)
|
|
|
|
|
Logger.LogTrace(exception, "Error stopping discord client!");
|
|
|
|
|
if (gatewayCts == null)
|
|
|
|
|
throw new InvalidOperationException("Discord gateway is not active!");
|
|
|
|
|
|
|
|
|
|
disconnectTcs.TrySetResult(null);
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
gatewayCts.Cancel();
|
|
|
|
|
var gatewayResult = await gatewayTask.ConfigureAwait(false);
|
|
|
|
|
gatewayTask = null;
|
|
|
|
|
if (!gatewayResult.IsSuccess)
|
|
|
|
|
Logger.LogWarning("Gateway issue: {0}", gatewayResult.Error.Message);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
client.Disconnected += DisconnectCallback;
|
|
|
|
|
|
|
|
|
|
await client.StopAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
Logger.LogTrace("Waiting for disconnect callback...");
|
|
|
|
|
using (cancellationToken.Register(() => disconnectTcs.SetCanceled()))
|
|
|
|
|
await disconnectTcs.Task.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
// https://github.com/discord-net/Discord.Net/blob/8afef8245cfd1f8b56956dd4b4577ed3c6904be5/src/Discord.Net.WebSocket/ConnectionManager.cs#L176
|
|
|
|
|
// State isn't set to disconnected until AFTER the callback fires
|
|
|
|
|
// Meaning if we check this.Connected right now it will still return true
|
|
|
|
|
// Yielding here will prevent this
|
|
|
|
|
await Task.Yield();
|
|
|
|
|
|
|
|
|
|
Logger.LogTrace("Stop async complete.");
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
client.Disconnected -= DisconnectCallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
var logoutTcs = new TaskCompletionSource<object>();
|
|
|
|
|
Task LogoutCallback()
|
|
|
|
|
{
|
|
|
|
|
logoutTcs.TrySetResult(null);
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client.LoggedOut += LogoutCallback;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await client.LogoutAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
Logger.LogTrace("Waiting for logout callback...");
|
|
|
|
|
using (cancellationToken.Register(() => logoutTcs.SetCanceled()))
|
|
|
|
|
await logoutTcs.Task.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
client.LoggedOut -= LogoutCallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("Disconnected!");
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning(e, "Error disconnecting from discord!");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Handle a message recieved from Discord.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="e">The <see cref="SocketMessage"/>.</param>
|
|
|
|
|
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
|
|
|
|
async Task Client_MessageReceived(SocketMessage e)
|
|
|
|
|
{
|
|
|
|
|
if (e.Author.Id == client.CurrentUser.Id)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
IDisposable typingState = null;
|
|
|
|
|
void StartTyping() => typingState = e.Channel.EnterTypingState();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (basedMeme && e.Content.Equals("Based on what?", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
StartTyping();
|
|
|
|
|
|
|
|
|
|
// DCT: None available
|
|
|
|
|
await SendMessage(
|
|
|
|
|
e.Channel.Id,
|
|
|
|
|
"https://youtu.be/LrNu-SuFF_o",
|
|
|
|
|
default)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pm = e.Channel is IPrivateChannel;
|
|
|
|
|
var shouldNotAnswer = !pm;
|
|
|
|
|
if (shouldNotAnswer)
|
|
|
|
|
lock (mappedChannels)
|
|
|
|
|
shouldNotAnswer = !mappedChannels.Contains(e.Channel.Id);
|
|
|
|
|
|
|
|
|
|
var content = NormalizeMentions(e.Content);
|
|
|
|
|
var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id)
|
|
|
|
|
|| (!shouldNotAnswer && content.Split(' ').First().Equals(ChatManager.CommonMention, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
if (mentionedUs)
|
|
|
|
|
StartTyping();
|
|
|
|
|
|
|
|
|
|
if (shouldNotAnswer)
|
|
|
|
|
{
|
|
|
|
|
if (mentionedUs)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = new Message
|
|
|
|
|
{
|
|
|
|
|
Content = content,
|
|
|
|
|
User = new ChatUser
|
|
|
|
|
{
|
|
|
|
|
RealId = e.Author.Id,
|
|
|
|
|
Channel = new ChannelRepresentation
|
|
|
|
|
{
|
|
|
|
|
RealId = e.Channel.Id,
|
|
|
|
|
IsPrivateChannel = pm,
|
|
|
|
|
ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
|
|
|
|
|
FriendlyName = e.Channel.Name,
|
|
|
|
|
|
|
|
|
|
// isAdmin and Tag populated by manager
|
|
|
|
|
},
|
|
|
|
|
FriendlyName = e.Author.Username,
|
|
|
|
|
Mention = NormalizeMentions(e.Author.Mention),
|
|
|
|
|
},
|
|
|
|
|
Context = typingState,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
EnqueueMessage(result);
|
|
|
|
|
typingState = null;
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
typingState?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
gatewayCts.Dispose();
|
|
|
|
|
gatewayCts = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|