Commit 75b32e03 authored by June's avatar June
Browse files

Detect banned users, add logging

parent 1ecf0c3d
......@@ -28,9 +28,10 @@
private readonly IHostEnvironment _hostEnvironment;
private readonly IDistributedCache _distributedCache;
private readonly IWordFilterEngine _wordFilterEngine;
private readonly IDiscordApi _discordApi;
private readonly Database _database;
public CommandParser(ILogger<CommandParser> logger, IDatabaseApi databaseApi, ArrangementGenerator arrangementGenerator, InteractionManager interactionManager, IHostEnvironment hostEnvironment, IDistributedCache distributedCache, IWordFilterEngine wordFilterEngine)
public CommandParser(ILogger<CommandParser> logger, IDatabaseApi databaseApi, ArrangementGenerator arrangementGenerator, InteractionManager interactionManager, IHostEnvironment hostEnvironment, IDistributedCache distributedCache, IWordFilterEngine wordFilterEngine, IDiscordApi discordApi)
{
_databaseApi = databaseApi;
_database = _databaseApi.Current;
......@@ -61,6 +62,7 @@
_hostEnvironment = hostEnvironment;
_distributedCache = distributedCache;
_wordFilterEngine = wordFilterEngine;
_discordApi = discordApi;
}
private static bool IsCardinal(string s)
......@@ -515,6 +517,8 @@
string content = command.Substring(verb.Length + 1);
if (_wordFilterEngine.MessageContainsBadWord(content))
{
await _discordApi.LogUserMessage(player.DiscordId, content, true);
string message = "<" + _database.GlobalText.SelectFromList("other_says_blocked");
Meeting meeting = _interactionManager.FindMeeting(player);
foreach (UserModel other in meeting._users)
......@@ -529,6 +533,8 @@
}
else
{
await _discordApi.LogUserMessage(player.DiscordId, content, false);
string message = "<" + _database.GlobalText.SelectFromList("other_says").Replace("__CONTENT__", content);
Meeting meeting = _interactionManager.FindMeeting(player);
foreach (UserModel other in meeting._users)
......
......@@ -30,9 +30,10 @@
private readonly CommandParser _commandParser;
private ArrangementGenerator _arrangementGenerator;
private readonly IDistributedCache _distributedCache;
private readonly IDiscordApi _discordApi;
private Task _arrangementGeneratorTask;
public GameInstance(IDatabaseApi databaseApi, ILogger<GameInstance> logger, CommandParser commandParser, ArrangementGenerator arrangementGenerator, IDistributedCache distributedCache)
public GameInstance(IDatabaseApi databaseApi, ILogger<GameInstance> logger, CommandParser commandParser, ArrangementGenerator arrangementGenerator, IDistributedCache distributedCache, IDiscordApi discordApi)
{
_players = new Dictionary<ulong, ConnectionInfo>();
_databaseApi = databaseApi;
......@@ -40,6 +41,7 @@
_commandParser = commandParser;
_arrangementGenerator = arrangementGenerator;
_distributedCache = distributedCache;
_discordApi = discordApi;
// TODO: put this in a better place
// Also schedule it to be called at some interval
_arrangementGenerator.Generate();
......@@ -67,8 +69,18 @@
public async Task AcceptPlayer(UserModel incomingUser, WebSocket webSocket, TaskCompletionSource<object> socketFinishedTsc)
{
var banned = await _discordApi.IsUserBanned(incomingUser.DiscordId);
if (banned)
{
_logger.LogInformation($"Rejecting player {incomingUser.DiscordId}:{incomingUser.Username} because they are banned.");
socketFinishedTsc.SetResult(new object());
return;
}
_logger.LogInformation($"Accepting player {incomingUser.DiscordId}:{incomingUser.Username}");
await _discordApi.LogUserConnected(incomingUser.DiscordId);
_players[incomingUser.DiscordId] = new ConnectionInfo
{
User = incomingUser,
......@@ -96,19 +108,31 @@
_players[incomingUser.DiscordId].ReceiveTask = Task.Run(async () =>
{
var buffer = new byte[1024 * 4];
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
try
{
if (result.MessageType == WebSocketMessageType.Text && result.EndOfMessage)
var buffer = new byte[1024 * 4];
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await ProcessInputFromPlayer(_players[incomingUser.DiscordId], Encoding.UTF8.GetString(buffer, 0, result.Count));
}
if (result.MessageType == WebSocketMessageType.Text && result.EndOfMessage)
{
await ProcessInputFromPlayer(_players[incomingUser.DiscordId], Encoding.UTF8.GetString(buffer, 0, result.Count));
}
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
socketFinishedTsc.TrySetResult(new object());
}
catch (Exception ex)
{
_logger.LogError("Uncaught exception in receive loop for WebSocket: " + ex.Message + "\n" + ex.StackTrace);
}
finally
{
_players.Remove(incomingUser.DiscordId);
await _discordApi.LogUserDisconnected(incomingUser.DiscordId);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
socketFinishedTsc.TrySetResult(new object());
});
//return Task.CompletedTask;
......
......@@ -24,19 +24,21 @@
private readonly ILogger<InteractionManager> _logger;
private readonly ArrangementGenerator _arrangementGenerator;
private readonly IDistributedCache _distributedCache;
private readonly IDiscordApi _discordApi;
private List<Meeting> _meetings;
private GameInstance _gameInstance;
// key is player discord ID, value is a dictionary whose key is another
// other player's discord ID and value is the UNIX timestamp when they were last together
private Dictionary<ulong, Dictionary<ulong, long>> _lastMetRecord;
public InteractionManager(IDatabaseApi databaseApi, ILogger<InteractionManager> logger, ArrangementGenerator arrangementGenerator, IDistributedCache distributedCache)
public InteractionManager(IDatabaseApi databaseApi, ILogger<InteractionManager> logger, ArrangementGenerator arrangementGenerator, IDistributedCache distributedCache, IDiscordApi discordApi)
{
_databaseApi = databaseApi;
_database = _databaseApi.Current;
_logger = logger;
_arrangementGenerator = arrangementGenerator;
_distributedCache = distributedCache;
_discordApi = discordApi;
_meetings = new List<Meeting>();
_lastMetRecord = new Dictionary<ulong, Dictionary<ulong, long>>();
}
......@@ -79,7 +81,13 @@
public async Task<string> CheckForInteraction(GameInstance gameInstance, UserModel player)
{
if (_gameInstance == null)
_gameInstance = gameInstance;
_gameInstance = gameInstance;
var banned = await _discordApi.IsUserBanned(player.DiscordId);
if (banned)
{
// Prevent a user that was banned from ever interacting with anyone else ever again. This handles scenarios where a user is banned mid-play session.
return null;
}
foreach (UserModel otherUser in gameInstance.GetPlayers())
{
_logger.LogInformation($"ID: {player.DiscordId} {otherUser.DiscordId} {player.DiscordId != otherUser.DiscordId} object: {player != otherUser}");
......@@ -88,7 +96,14 @@
&& (player.DiscordId != otherUser.DiscordId)
&& (otherUser.InMeeting == false)
&& !LastMeetingTooSoon(player, otherUser))
{
{
var otherBanned = await _discordApi.IsUserBanned(otherUser.DiscordId);
if (otherBanned)
{
// Prevent a user that was banned from ever interacting with anyone else ever again. This handles scenarios where a user is banned mid-play session.
continue;
}
player.InMeeting = true;
otherUser.InMeeting = true;
Meeting meeting = new Meeting(this);
......
......@@ -80,7 +80,7 @@
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Discord" Version="5.0.0" />
<PackageReference Include="Discord.Net.Rest" Version="2.2.0" />
<PackageReference Include="Discord.Net" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.2" />
<PackageReference Include="YamlDotNet" Version="9.1.4" />
</ItemGroup>
......
namespace MomentaryMeeting.Services
{
using Discord.Rest;
using Discord.WebSocket;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public interface IDiscordApi
{
Task<bool> JoinGuildAndCheckPermittedToPlay(ulong userId, string userAccessToken);
Task<bool> IsUserBanned(ulong userId);
Task LogUserMessage(ulong userId, string contentPosted, bool flaggedByBadWordFilter);
Task LogUserConnected(ulong userId);
Task LogUserDisconnected(ulong userId);
}
public class DiscordApi : IDiscordApi
{
private readonly IConfiguration _configuration;
private readonly ILogger<DiscordApi> _logger;
private readonly IWebHostEnvironment _webHostEnvironment;
private DiscordSocketClient _socketClient;
private DiscordRestClient _restClient;
private HashSet<ulong> _banList;
private const ulong _discordRedpointGuild = 449194702005207040;
private const ulong _discordLogChannel = 805603665430970400;
public DiscordApi(IConfiguration configuration, ILogger<DiscordApi> logger, IWebHostEnvironment webHostEnvironment)
{
_configuration = configuration;
_logger = logger;
_webHostEnvironment = webHostEnvironment;
_banList = null;
}
private async Task<DiscordRestClient> GetClient()
{
if (_restClient != null)
{
return _restClient;
}
var botToken = _configuration.GetValue<string>("Discord:BotToken");
if (!string.IsNullOrWhiteSpace(botToken))
{
var client = new DiscordRestClient();
await client.LoginAsync(Discord.TokenType.Bot, botToken);
_restClient = client;
// we also have to have authenticated to a gateway at least once for the REST SendMessage endpoint to work...
var socketClient = new DiscordSocketClient(new DiscordSocketConfig
{
GatewayIntents = Discord.GatewayIntents.GuildBans,
});
socketClient.UserBanned += SocketClient_UserBanned;
socketClient.UserUnbanned += SocketClient_UserBanned;
await socketClient.LoginAsync(Discord.TokenType.Bot, botToken);
await socketClient.StartAsync();
_socketClient = socketClient;
return client;
}
return null;
}
private Task SocketClient_UserBanned(SocketUser arg1, SocketGuild arg2)
{
// Ban list changed, flush cache and fetch bans at next check.
_logger.LogInformation("The ban list was updated, and will be refreshed on next check.");
_banList = null;
return Task.CompletedTask;
}
public async Task<bool> JoinGuildAndCheckPermittedToPlay(ulong userId, string userAccessToken)
{
if (_banList != null && _banList.Contains(userId))
{
_logger.LogError("A banned user attempted to sign in and was prevented.");
return false;
}
var client = await GetClient();
if (client == null)
{
_logger.LogError("Discord API not available, can not authenticate user!");
return false;
}
var guild = await client.GetGuildAsync(_discordRedpointGuild);
if (guild == null)
{
_logger.LogError("Can not find Redpoint guild, unable to authenticate.");
return false;
}
_banList = (await guild.GetBansAsync()).Select(x => x.User.Id).ToHashSet();
if (_banList.Contains(userId))
{
_logger.LogError("A banned user attempted to sign in and was prevented.");
return false;
}
var existingUser = await client.GetGuildUserAsync(_discordRedpointGuild, userId);
if (existingUser == null)
{
var newUser = await guild.AddGuildUserAsync(userId, userAccessToken);
if (newUser == null)
{
_logger.LogError("The user could not be joined to the Redpoint guild, but they don't already exist in the guild.");
return false;
}
// Otherwise user is considered authenticated.
return true;
}
// Otherwise user is considered authenticated.
return true;
}
public async Task<bool> IsUserBanned(ulong userId)
{
if (_banList != null && _banList.Contains(userId))
{
_logger.LogError("Detected that a user was banned in the Discord!");
return true;
}
var client = await GetClient();
if (client == null)
{
_logger.LogError("Discord API not available, can not check user for ban!");
return false;
}
var guild = await client.GetGuildAsync(_discordRedpointGuild);
if (guild == null)
{
_logger.LogError("Can not find Redpoint guild, unable to authenticate.");
return false;
}
_banList = (await guild.GetBansAsync()).Select(x => x.User.Id).ToHashSet();
if (_banList.Contains(userId))
{
_logger.LogError("Detected that a user was banned in the Discord!");
return true;
}
return false;
}
public async Task LogUserMessage(ulong userId, string contentPosted, bool flaggedByBadWordFilter)
{
var client = await GetClient();
if (client == null)
{
return;
}
var guild = await client.GetGuildAsync(_discordRedpointGuild);
if (guild == null)
{
return;
}
var textChannel = await guild.GetTextChannelAsync(_discordLogChannel);
if (textChannel == null)
{
return;
}
if (flaggedByBadWordFilter)
{
await textChannel.SendMessageAsync(":face_with_symbols_over_mouth: <@" + userId + "> has their message flagged by bad word filter: `" + contentPosted.Replace("`", "") + "`");
}
else
{
await textChannel.SendMessageAsync("<@" + userId + "> sent: `" + contentPosted.Replace("`", "") + "`");
}
}
public async Task LogUserConnected(ulong userId)
{
var client = await GetClient();
if (client == null)
{
return;
}
var guild = await client.GetGuildAsync(_discordRedpointGuild);
if (guild == null)
{
return;
}
var textChannel = await guild.GetTextChannelAsync(_discordLogChannel);
if (textChannel == null)
{
return;
}
await textChannel.SendMessageAsync("<@" + userId + "> started playing.");
}
public async Task LogUserDisconnected(ulong userId)
{
var client = await GetClient();
if (client == null)
{
return;
}
var guild = await client.GetGuildAsync(_discordRedpointGuild);
if (guild == null)
{
return;
}
var textChannel = await guild.GetTextChannelAsync(_discordLogChannel);
if (textChannel == null)
{
return;
}
await textChannel.SendMessageAsync("<@" + userId + "> disconnected.");
}
}
}
......@@ -39,6 +39,7 @@ namespace MomentaryMeeting
services.AddSingleton<ArrangementGenerator, ArrangementGenerator>();
services.AddSingleton<InteractionManager, InteractionManager>();
services.AddSingleton<IWordFilterEngine, WordFilterEngine>();
services.AddSingleton<IDiscordApi, DiscordApi>();
if (_env.IsProduction())
{
......@@ -94,21 +95,11 @@ namespace MomentaryMeeting
var userId = ulong.Parse(ctx.Identity.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value);
var accessToken = tokens.First(x => x.Name == "access_token").Value;
try
var discordApi = ctx.HttpContext.RequestServices.GetRequiredService<IDiscordApi>();
var allowLogin = await discordApi.JoinGuildAndCheckPermittedToPlay(userId, accessToken);
if (!allowLogin)
{
using (var client = new DiscordRestClient())
{
await client.LoginAsync(TokenType.Bot, _configuration.GetValue<string>("Discord:BotToken"));
await client.AddGuildUserAsync(
449194702005207040,
userId,
accessToken);
}
}
catch
{
// Ignore errors.
ctx.Fail("You could not be authenticated with Discord. Ensure that you are not at the server limit and are not banned. Alternatively, join the Discord directly by going to https://discord.gg/wKR82eK.");
}
};
})
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment