Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP (but working) Discord implementation #95

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
107 changes: 107 additions & 0 deletions src/DevChatter.Bot.Core/Util/CommandParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Collections.Generic;

namespace DevChatter.Bot.Core.Util
{
public static class CommandParser
{
public static (string commandWord, List<string> arguments) Parse(string commandString, int startIndex = 0)
{
if (startIndex < 0 || string.IsNullOrWhiteSpace(commandString))
{
return (string.Empty, new List<string>());
}

int commandWordEndIndex = commandString.IndexOf(' ', startIndex);
if (commandWordEndIndex == -1)
{
commandWordEndIndex = commandString.Length - 1;
}

int commandWordLength = commandWordEndIndex - startIndex + 1;
string commandWord = commandString.Substring(startIndex, commandWordLength).Trim();
if (commandWordEndIndex == commandString.Length - 1)
{
// return early because we have no arguments
return (commandWord, new List<string>());
}

string remainingCommand = commandString.Substring(commandWordLength).Trim();
var arguments = SplitArguments(remainingCommand);

return (commandWord, arguments);
}

private static List<string> SplitArguments(string arguments)
{
const char argumentDelimiter = ' ';
const char quoteCharacter = '"';

if (string.IsNullOrWhiteSpace(arguments))
return new List<string>();

List<string> splitArguments = new List<string>();
bool wasInQuotedSection = false;
bool inQuotedSection = false;
int argumentStart = -1;

for (int i = 0; i < arguments.Length; ++i)
{
if (arguments[i] == quoteCharacter)
{
if (!inQuotedSection)
{
if (i == 0 || arguments[i - 1] == argumentDelimiter)
{
inQuotedSection = true;
wasInQuotedSection = false;
}
}
else
{
if (i == arguments.Length - 1 || arguments[i + 1] == argumentDelimiter)
{
wasInQuotedSection = true;
inQuotedSection = false;
}
}
}
else if(arguments[i] == argumentDelimiter && !inQuotedSection)
{
int argumentLength = i - argumentStart;
if (wasInQuotedSection)
{
// If we were previously in a quoted section we
// need to offset our length by -1 otherwise we get the closing quote
argumentLength -= 1;
}

var argument = arguments.Substring(argumentStart, argumentLength);
splitArguments.Add(argument);
argumentStart = -1;
wasInQuotedSection = false;
inQuotedSection = false;
}
else if(arguments[i] != argumentDelimiter && argumentStart == -1)
{
// we only want to record the start index
// if the current character is NOT the delimiter
argumentStart = i;
}
}

if(argumentStart != -1)
{
int argumentLength = arguments.Length - argumentStart;
if(wasInQuotedSection)
{
argumentLength -= 1;
}

var argument = arguments.Substring(argumentStart, argumentLength);
splitArguments.Add(argument);
}

return splitArguments;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Autofac" Version="4.6.2" />
<PackageReference Include="Discord.Net" Version="1.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DevChatter.Bot.Core\DevChatter.Bot.Core.csproj" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/DevChatter.Bot.Infra.Discord/DevChatterBotDiscordModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Autofac;

namespace DevChatter.Bot.Infra.Discord
{
public class DevChatterBotDiscordModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<DiscordChatClient>().AsImplementedInterfaces().SingleInstance();
}
}
}
230 changes: 230 additions & 0 deletions src/DevChatter.Bot.Infra.Discord/DiscordChatClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DevChatter.Bot.Core.Data.Model;
using DevChatter.Bot.Core.Events.Args;
using DevChatter.Bot.Core.Systems.Chat;
using DevChatter.Bot.Core.Util;
using DevChatter.Bot.Infra.Discord.Extensions;
using Discord;
using Discord.Commands;
using Discord.WebSocket;

namespace DevChatter.Bot.Infra.Discord
{
public class DiscordChatClient : IChatClient
{
private readonly DiscordClientSettings _settings;
private readonly DiscordSocketClient _discordClient;
private TaskCompletionSource<bool> _connectionCompletionTask = new TaskCompletionSource<bool>();
private TaskCompletionSource<bool> _disconnectionCompletionTask = new TaskCompletionSource<bool>();
private SocketGuild _guild;
private readonly List<ulong> _guildChannels = new List<ulong>();
private ISocketMessageChannel _textChannel;
private bool _isReady;

public DiscordChatClient(DiscordClientSettings settings)
{
_settings = settings;
_discordClient = new DiscordSocketClient();
_discordClient.MessageReceived += DiscordClientMessageReceived;
_discordClient.GuildAvailable += DiscordClientGuildAvailable;
_discordClient.GuildUnavailable += DiscordClientGuildUnavailable;
_discordClient.ChannelCreated += DiscordClientChannelCreated;
_discordClient.ChannelDestroyed += DiscordClientChannelDestroyed;
_discordClient.UserJoined += DiscordClientUserJoined;
_discordClient.UserLeft += DiscordClientUserLeft;
}

private async Task DiscordClientGuildAvailable(SocketGuild guild)
{
_guild = guild;
_guildChannels.AddRange(_guild.Channels.Select(channel => channel.Id));
_textChannel = _guild.Channels.FirstOrDefault(channel => channel.Id == _settings.DiscordTextChannelId) as ISocketMessageChannel;
_isReady = true;
}

private async Task DiscordClientGuildUnavailable(SocketGuild guild)
{
_guild = null;
_guildChannels.Clear();
_isReady = false;
}

private async Task DiscordClientChannelCreated(SocketChannel newChannel)
{
_guildChannels.Add(newChannel.Id);
}

private async Task DiscordClientChannelDestroyed(SocketChannel oldChannel)
{
_guildChannels.Remove(oldChannel.Id);
}

private async Task DiscordClientMessageReceived(SocketMessage arg)
{
var message = arg as SocketUserMessage;
if (message == null)
{
return;
}

int commandStartIndex = 0;
if (message.HasCharPrefix(_settings.CommandPrefix, ref commandStartIndex))
{
if (_guildChannels.Contains(message.Channel.Id))
{
if (arg.Author is IGuildUser guildUser)
{
GuildCommandReceived(guildUser, commandStartIndex, arg.Content);
}
}
else
{
DirectCommandReceieved(arg.Author, commandStartIndex, arg.Content);
}
}
}

private void GuildCommandReceived(IGuildUser user, int commandStartIndex, string message)
{
var commandInfo = CommandParser.Parse(message, commandStartIndex);
if(string.IsNullOrWhiteSpace(commandInfo.commandWord))
{
return;
}

RaiseOnCommandReceived(user, commandInfo.commandWord, commandInfo.arguments);
}

private void DirectCommandReceieved(IUser user, int commandStartIndex, string message)
{
var commandInfo = CommandParser.Parse(message, commandStartIndex);
if(string.IsNullOrWhiteSpace(commandInfo.commandWord))
{
return;
}

// TODO: Do we want to handle direct message commands?
}

public async Task Connect()
{
_discordClient.Connected += DiscordClientConnected;
await _discordClient.LoginAsync(TokenType.Bot, _settings.DiscordToken).ConfigureAwait(false);
await _discordClient.StartAsync().ConfigureAwait(false);

await _connectionCompletionTask.Task;
}

private async Task DiscordClientConnected()
{
_discordClient.Connected -= DiscordClientConnected;

_connectionCompletionTask?.SetResult(true);
_disconnectionCompletionTask = new TaskCompletionSource<bool>();
}

public async Task Disconnect()
{
_discordClient.Disconnected += DiscordClientDisconnected;
await _discordClient.LogoutAsync().ConfigureAwait(false);
await _discordClient.StopAsync().ConfigureAwait(false);

await _disconnectionCompletionTask.Task;
}

private async Task DiscordClientDisconnected(Exception arg)
{
_discordClient.Disconnected -= DiscordClientDisconnected;

_disconnectionCompletionTask.SetResult(true);
_connectionCompletionTask = new TaskCompletionSource<bool>();
}

private async Task DiscordClientUserJoined(SocketGuildUser arg)
{
RaiseOnUserNoticed(arg);
}

private async Task DiscordClientUserLeft(SocketGuildUser arg)
{
RaiseOnUserLeft(arg);
}

/// <summary>
/// If not connected to a guild
/// </summary>
public IList<ChatUser> GetAllChatters()
{
if (!_isReady)
{
return new List<ChatUser>();
}

var chatUsers = _guild.Users.Select(user => user.ToChatUser(_settings)).ToList();
return chatUsers;
}

public void SendMessage(string message)
{
if (!_isReady)
{
return;
}

_textChannel?.SendMessageAsync($"`{message}`").Wait();
}

public void SendDirectMessage(string username, string message)
{
if (!_isReady)
{
return;
}

var discordUser = _guild.Users.FirstOrDefault(u => u.Username == username);
discordUser?.SendMessageAsync(message);
}

private void RaiseOnCommandReceived(IGuildUser user, string commandWord, List<string> arguments)
{
var eventArgs = new CommandReceivedEventArgs
{
CommandWord = commandWord,
Arguments = arguments ?? new List<string>(),
ChatUser = user.ToChatUser(_settings)
};

OnCommandReceived?.Invoke(this, eventArgs);
}

private void RaiseOnUserNoticed(SocketGuildUser user)
{
var eventArgs = new UserStatusEventArgs
{
DisplayName = user.Username,
Role = user.ToUserRole(_settings)
};

OnUserNoticed?.Invoke(this, eventArgs);
}

private void RaiseOnUserLeft(SocketGuildUser user)
{
var eventArgs = new UserStatusEventArgs
{
DisplayName = user.Username,
Role = user.ToUserRole(_settings)
};

OnUserLeft?.Invoke(this, eventArgs);
}

public event EventHandler<CommandReceivedEventArgs> OnCommandReceived;
public event EventHandler<NewSubscriberEventArgs> OnNewSubscriber;
public event EventHandler<UserStatusEventArgs> OnUserNoticed;
public event EventHandler<UserStatusEventArgs> OnUserLeft;
}
}
12 changes: 12 additions & 0 deletions src/DevChatter.Bot.Infra.Discord/DiscordClientSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace DevChatter.Bot.Infra.Discord
{
public class DiscordClientSettings
{
public string DiscordToken { get; set; }
public ulong DiscordStreamerRoleId { get; set; }
public ulong DiscordModeratorRoleId { get; set; }
public ulong DiscordSubscriberRoleId { get; set; }
public ulong DiscordTextChannelId { get; set; }
public char CommandPrefix { get; set; }
}
}
Loading