Browse Source

Merge 9a783df90a into acc192c689

pull/9/merge
Googie2149 9 years ago
parent
commit
2b4072d881
35 changed files with 966 additions and 517 deletions
  1. +1
    -1
      README.md
  2. +14
    -5
      src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
  3. +96
    -6
      src/Discord.Net.Commands/Command.cs
  4. +73
    -57
      src/Discord.Net.Commands/CommandBuilder.cs
  5. +8
    -0
      src/Discord.Net.Commands/CommandExtensions.cs
  6. +97
    -0
      src/Discord.Net.Commands/CommandMap.cs
  7. +79
    -53
      src/Discord.Net.Commands/CommandParser.cs
  8. +54
    -0
      src/Discord.Net.Commands/CommandService.Events.cs
  9. +184
    -0
      src/Discord.Net.Commands/CommandService.cs
  10. +49
    -0
      src/Discord.Net.Commands/CommandServiceConfig.cs
  11. +0
    -60
      src/Discord.Net.Commands/CommandsPlugin.Events.cs
  12. +0
    -138
      src/Discord.Net.Commands/CommandsPlugin.cs
  13. +5
    -2
      src/Discord.Net.Net45/Discord.Net.csproj
  14. +6
    -0
      src/Discord.Net/API/Enums/ChannelType.cs
  15. +6
    -0
      src/Discord.Net/API/Enums/PermissionTarget.cs
  16. +6
    -0
      src/Discord.Net/API/Enums/Region.cs
  17. +1
    -39
      src/Discord.Net/API/Enums/StringEnum.cs
  18. +6
    -0
      src/Discord.Net/API/Enums/UserStatus.cs
  19. +20
    -16
      src/Discord.Net/DiscordClient.Channels.cs
  20. +24
    -12
      src/Discord.Net/DiscordClient.Messages.cs
  21. +1
    -1
      src/Discord.Net/DiscordClient.Servers.cs
  22. +33
    -10
      src/Discord.Net/DiscordClient.Users.cs
  23. +20
    -3
      src/Discord.Net/DiscordClient.cs
  24. +28
    -19
      src/Discord.Net/Helpers/Mention.cs
  25. +7
    -0
      src/Discord.Net/IService.cs
  26. +7
    -7
      src/Discord.Net/Models/Channel.cs
  27. +4
    -0
      src/Discord.Net/Models/Color.cs
  28. +2
    -0
      src/Discord.Net/Models/GlobalUser.cs
  29. +66
    -63
      src/Discord.Net/Models/Invite.cs
  30. +50
    -17
      src/Discord.Net/Models/Message.cs
  31. +8
    -0
      src/Discord.Net/Models/Permissions.cs
  32. +2
    -0
      src/Discord.Net/Models/Role.cs
  33. +2
    -0
      src/Discord.Net/Models/Server.cs
  34. +4
    -1
      src/Discord.Net/Models/User.cs
  35. +3
    -7
      src/Discord.Net/Net/Rest/SharpRestEngine.cs

+ 1
- 1
README.md View File

@@ -1,4 +1,4 @@
# Discord.Net v0.8.0-beta1
# Discord.Net v0.8.1-Beta
An unofficial .Net API Wrapper for the Discord client (http://discordapp.com).

Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx).


+ 14
- 5
src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj View File

@@ -7,7 +7,7 @@
<ProjectGuid>{1B5603B4-6F8F-4289-B945-7BAAE523D740}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Discord</RootNamespace>
<RootNamespace>Discord.Commands</RootNamespace>
<AssemblyName>Discord.Net.Commands</AssemblyName>
<FileAlignment>512</FileAlignment>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
@@ -43,14 +43,23 @@
<Compile Include="..\Discord.Net.Commands\CommandBuilder.cs">
<Link>CommandBuilder.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandExtensions.cs">
<Link>CommandExtensions.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandMap.cs">
<Link>CommandMap.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandParser.cs">
<Link>CommandParser.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandsPlugin.cs">
<Link>CommandsPlugin.cs</Link>
<Compile Include="..\Discord.Net.Commands\CommandService.cs">
<Link>CommandService.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandService.Events.cs">
<Link>CommandService.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandsPlugin.Events.cs">
<Link>CommandsPlugin.Events.cs</Link>
<Compile Include="..\Discord.Net.Commands\CommandServiceConfig.cs">
<Link>CommandServiceConfig.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>


+ 96
- 6
src/Discord.Net.Commands/Command.cs View File

@@ -1,21 +1,111 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Commands
{
public enum ParameterType
{
/// <summary> Catches a single required parameter. </summary>
Required,
/// <summary> Catches a single optional parameter. </summary>
Optional,
/// <summary> Catches a zero or more optional parameters. </summary>
Multiple,
/// <summary> Catches all remaining text as a single optional parameter. </summary>
Unparsed
}
public sealed class CommandParameter
{
public string Name { get; }
public ParameterType Type { get; }

public CommandParameter(string name, ParameterType type)
{
Name = name;
Type = type;
}
}

public sealed class Command
{
public string Text { get; }
public int? MinArgs { get; internal set; }
public int? MaxArgs { get; internal set; }
public int MinPerms { get; internal set; }
internal readonly string[] Parts;
internal Func<CommandEventArgs, Task> Handler;
public int? MinArgs { get; private set; }
public int? MaxArgs { get; private set; }
public int MinPermissions { get; internal set; }
public bool IsHidden { get; internal set; }
public string Description { get; internal set; }

public IEnumerable<string> Aliases => _aliases;
private string[] _aliases;

public IEnumerable<CommandParameter> Parameters => _parameters;
internal CommandParameter[] _parameters;

private Func<CommandEventArgs, Task> _handler;

internal Command(string text)
{
Text = text;
Parts = text.ToLowerInvariant().Split(' ');
IsHidden = false;
_aliases = new string[0];
_parameters = new CommandParameter[0];
}

internal void SetAliases(string[] aliases)
{
_aliases = aliases;
}

internal void SetParameters(CommandParameter[] parameters)
{
_parameters = parameters;
if (parameters != null)
{
if (parameters.Length == 0)
{
MinArgs = 0;
MaxArgs = 0;
}
else
{
if (parameters[parameters.Length - 1].Type == ParameterType.Multiple)
MaxArgs = null;
else
MaxArgs = parameters.Length;

int? optionalStart = null;
for (int i = parameters.Length - 1; i >= 0; i--)
{
if (parameters[i].Type == ParameterType.Optional)
optionalStart = i;
else
break;
}
if (optionalStart == null)
MinArgs = MaxArgs;
else
MinArgs = optionalStart.Value;
}
}
}

internal void SetHandler(Func<CommandEventArgs, Task> func)
{
_handler = func;
}
internal void SetHandler(Action<CommandEventArgs> func)
{
_handler = e => { func(e); return TaskHelper.CompletedTask; };
}
internal Task Run(CommandEventArgs args)
{
var task = _handler(args);
if (task != null)
return task;
else
return TaskHelper.CompletedTask;
}
}
}

+ 73
- 57
src/Discord.Net.Commands/CommandBuilder.cs View File

@@ -1,79 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Commands
{
public sealed class CommandBuilder
{
private readonly CommandService _service;
private readonly Command _command;
public CommandBuilder(Command command)
private List<CommandParameter> _params;
private bool _allowRequired, _isClosed;
private string _prefix;

public CommandBuilder(CommandService service, Command command, string prefix)
{
_service = service;
_command = command;
}
public CommandBuilder ArgsEqual(int argCount)
{
_command.MinArgs = argCount;
_command.MaxArgs = argCount;
return this;
}
public CommandBuilder ArgsAtLeast(int minArgCount)
{
_command.MinArgs = minArgCount;
_command.MaxArgs = null;
return this;
}
public CommandBuilder ArgsAtMost(int maxArgCount)
_params = new List<CommandParameter>();
_prefix = prefix;
_allowRequired = true;
_isClosed = false;
}

public CommandBuilder Alias(params string[] aliases)
{
_command.MinArgs = null;
_command.MaxArgs = maxArgCount;
aliases = aliases.Select(x => AppendPrefix(_prefix, x)).ToArray();
_command.SetAliases(aliases);
return this;
}
public CommandBuilder ArgsBetween(int minArgCount, int maxArgCount)
public CommandBuilder Info(string description)
{
_command.MinArgs = minArgCount;
_command.MaxArgs = maxArgCount;
_command.Description = description;
return this;
}
public CommandBuilder NoArgs()
public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required)
{
_command.MinArgs = 0;
_command.MaxArgs = 0;
if (_isClosed)
throw new Exception($"No parameters may be added after a {nameof(ParameterType.Multiple)} or {nameof(ParameterType.Unparsed)} parameter.");
if (!_allowRequired && type == ParameterType.Required)
throw new Exception($"{nameof(ParameterType.Required)} parameters may not be added after an optional one");

_params.Add(new CommandParameter(name, type));

if (type == ParameterType.Optional)
_allowRequired = false;
if (type == ParameterType.Multiple || type == ParameterType.Unparsed)
_isClosed = true;
return this;
}
public CommandBuilder AnyArgs()
public CommandBuilder Hide()
{
_command.MinArgs = null;
_command.MaxArgs = null;
_command.IsHidden = true;
return this;
}

public CommandBuilder MinPermissions(int level)
{
_command.MinPerms = level;
_command.MinPermissions = level;
return this;
}

public CommandBuilder Do(Func<CommandEventArgs, Task> func)
public void Do(Func<CommandEventArgs, Task> func)
{
_command.Handler = func;
return this;
_command.SetHandler(func);
Build();
}
public CommandBuilder Do(Action<CommandEventArgs> func)
public void Do(Action<CommandEventArgs> func)
{
_command.Handler = e => { func(e); return TaskHelper.CompletedTask; };
return this;
_command.SetHandler(func);
Build();
}
private void Build()
{
_command.SetParameters(_params.ToArray());
foreach (var alias in _command.Aliases)
_service.Map.AddCommand(alias, _command);
_service.AddCommand(_command);
}

internal static string AppendPrefix(string prefix, string cmd)
{
if (cmd != "")
{
if (prefix != "")
return prefix + ' ' + cmd;
else
return cmd;
}
else
{
if (prefix != "")
return prefix;
else
throw new ArgumentOutOfRangeException(nameof(cmd));
}
}
}
public sealed class CommandGroupBuilder
{
private readonly CommandsPlugin _plugin;
internal readonly CommandService _service;
private readonly string _prefix;
private int _defaultMinPermissions;

internal CommandGroupBuilder(CommandsPlugin plugin, string prefix, int defaultMinPermissions)
internal CommandGroupBuilder(CommandService service, string prefix, int defaultMinPermissions)
{
_plugin = plugin;
_service = service;
_prefix = prefix;
_defaultMinPermissions = defaultMinPermissions;
}
@@ -85,32 +117,16 @@ namespace Discord.Commands

public CommandGroupBuilder CreateCommandGroup(string cmd, Action<CommandGroupBuilder> config = null)
{
config(new CommandGroupBuilder(_plugin, _prefix + ' ' + cmd, _defaultMinPermissions));
config(new CommandGroupBuilder(_service, _prefix + ' ' + cmd, _defaultMinPermissions));
return this;
}
public CommandBuilder CreateCommand()
=> CreateCommand("");
public CommandBuilder CreateCommand(string cmd)
{
string text;
if (cmd != "")
{
if (_prefix != "")
text = _prefix + ' ' + cmd;
else
text = cmd;
}
else
{
if (_prefix != "")
text = _prefix;
else
throw new ArgumentOutOfRangeException(nameof(cmd));
}
var command = new Command(text);
command.MinPerms = _defaultMinPermissions;
_plugin.AddCommand(command);
return new CommandBuilder(command);
var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd));
command.MinPermissions = _defaultMinPermissions;
return new CommandBuilder(_service, command, _prefix);
}
}
}

+ 8
- 0
src/Discord.Net.Commands/CommandExtensions.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Commands
{
public static class CommandExtensions
{
public static CommandService Commands(this DiscordClient client)
=> client.GetService<CommandService>();
}
}

+ 97
- 0
src/Discord.Net.Commands/CommandMap.cs View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Commands
{
internal class CommandMap
{
private CommandMap _parent;
private Command _command;
private readonly Dictionary<string, CommandMap> _subCommands;

public Command Command => _command;
public IEnumerable<Command> SubCommands => _subCommands.Select(x => x.Value.Command).Where(x => x != null);

public CommandMap(CommandMap parent)
{
_parent = parent;
_subCommands = new Dictionary<string, CommandMap>();
}
public CommandMap GetMap(string text)
{
return GetMap(0, text.Split(' '));
}
public CommandMap GetMap(int index, string[] parts)
{
if (index != parts.Length)
{
string nextPart = parts[index];
CommandMap nextGroup;
if (_subCommands.TryGetValue(nextPart, out nextGroup))
return nextGroup.GetMap(index + 1, parts);
else
return null;
}
return this;
}

public Command GetCommand()
{
if (_command != null)
return _command;
else if (_parent != null)
return _parent.GetCommand();
else
return null;
}
public Command GetCommand(string text)
{
return GetCommand(0, text.Split(' '));
}
public Command GetCommand(int index, string[] parts)
{
if (index != parts.Length)
{
string nextPart = parts[index];
CommandMap nextGroup;
if (_subCommands.TryGetValue(nextPart, out nextGroup))
{
var cmd = nextGroup.GetCommand(index + 1, parts);
if (cmd != null)
return cmd;
}
}

if (_command != null)
return _command;
return null;
}

public void AddCommand(string text, Command command)
{
AddCommand(0, text.Split(' '), command);
}
public void AddCommand(int index, string[] parts, Command command)
{
if (index != parts.Length)
{
string nextPart = parts[index];
CommandMap nextGroup;
if (!_subCommands.TryGetValue(nextPart, out nextGroup))
{
nextGroup = new CommandMap(this);
_subCommands.Add(nextPart, nextGroup);
}
nextGroup.AddCommand(index + 1, parts, command);
}
else
{
if (_command != null)
throw new InvalidOperationException("A command has already been added with this path.");
_command = command;
}
}
}
}

+ 79
- 53
src/Discord.Net.Commands/CommandParser.cs View File

@@ -2,51 +2,24 @@

namespace Discord.Commands
{
public class CommandPart
{
public string Value { get; }
public int Index { get; }

internal CommandPart(string value, int index)
{
Value = value;
Index = index;
}
}

//TODO: Check support for escaping
public static class CommandParser
internal static class CommandParser
{
private enum CommandParserPart
{
None,
CommandName,
Parameter,
QuotedParameter,
DoubleQuotedParameter
}

public static bool Parse(string input, out string command, out CommandPart[] args)
{
return Parse(input, out command, out args, true);
}
public static bool ParseArgs(string input, out CommandPart[] args)
{
string ignored;
return Parse(input, out ignored, out args, false);
}

private static bool Parse(string input, out string command, out CommandPart[] args, bool parseCommand)
public static bool ParseCommand(string input, CommandMap map, out Command command, out int endPos)
{
CommandParserPart currentPart = parseCommand ? CommandParserPart.CommandName : CommandParserPart.None;
int startPosition = 0;
int endPosition = 0;
int inputLength = input.Length;
int inputLength = input.Length;
bool isEscaped = false;
List<CommandPart> argList = new List<CommandPart>();

command = null;
args = null;
endPos = 0;

if (input == "")
return false;
@@ -59,23 +32,71 @@ namespace Discord.Commands
else if (currentChar == '\\')
isEscaped = true;

switch (currentPart)
if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
{
case CommandParserPart.CommandName:
if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
string temp = input.Substring(startPosition, length);
if (temp == "")
startPosition = endPosition;
else
{
var newMap = map.GetMap(temp);
if (newMap != null)
{
int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
string temp = input.Substring(startPosition, length);
if (temp == "")
startPosition = endPosition;
else
{
currentPart = CommandParserPart.None;
command = temp;
startPosition = endPosition;
}
}
map = newMap;
endPos = endPosition;
}
else
break;
startPosition = endPosition;
}
}
}
command = map.GetCommand(); //Work our way backwards to find a command that matches our input
return command != null;
}

//TODO: Check support for escaping
public static CommandErrorType? ParseArgs(string input, int startPos, Command command, out string[] args)
{
CommandParserPart currentPart = CommandParserPart.None;
int startPosition = startPos;
int endPosition = startPos;
int inputLength = input.Length;
bool isEscaped = false;

var expectedArgs = command._parameters;
List<string> argList = new List<string>();
CommandParameter parameter = null;
args = null;

if (input == "")
return CommandErrorType.InvalidInput;

while (endPosition < inputLength)
{
if (startPosition == endPosition && (parameter == null || parameter.Type != ParameterType.Multiple)) //Is first char of a new arg
{
if (argList.Count == command.MaxArgs)
return CommandErrorType.BadArgCount;

parameter = command._parameters[argList.Count];
if (parameter.Type == ParameterType.Unparsed)
{
argList.Add(input.Substring(startPosition));
break;
}
}

char currentChar = input[endPosition++];
if (isEscaped)
isEscaped = false;
else if (currentChar == '\\')
isEscaped = true;

switch (currentPart)
{
case CommandParserPart.None:
if ((!isEscaped && currentChar == '\"'))
{
@@ -96,7 +117,7 @@ namespace Discord.Commands
else
{
currentPart = CommandParserPart.None;
argList.Add(new CommandPart(temp, startPosition));
argList.Add(temp);
startPosition = endPosition;
}
}
@@ -106,31 +127,36 @@ namespace Discord.Commands
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
currentPart = CommandParserPart.None;
argList.Add(new CommandPart(temp, startPosition));
argList.Add(temp);
startPosition = endPosition;
}
else if (endPosition >= inputLength)
return false;
return CommandErrorType.InvalidInput;
break;
case CommandParserPart.DoubleQuotedParameter:
if ((!isEscaped && currentChar == '\"'))
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
currentPart = CommandParserPart.None;
argList.Add(new CommandPart(temp, startPosition));
argList.Add(temp);
startPosition = endPosition;
}
else if (endPosition >= inputLength)
return false;
return CommandErrorType.InvalidInput;
break;
}
}

if (parseCommand && (command == null || command == ""))
return false;
if (argList.Count < command.MinArgs)
{
/*if (command._parameters[command._parameters.Length - 1].Type == ParameterType.Unparsed)
argList.Add("");
else*/
return CommandErrorType.BadArgCount;
}

args = argList.ToArray();
return true;
return null;
}
}
}

+ 54
- 0
src/Discord.Net.Commands/CommandService.Events.cs View File

@@ -0,0 +1,54 @@
using System;

namespace Discord.Commands
{
public class CommandEventArgs
{
public Message Message { get; }
public Command Command { get; }
public int? UserPermissions { get; }
public string[] Args { get; }
public User User => Message.User;
public Channel Channel => Message.Channel;
public Server Server => Message.Channel.Server;

public CommandEventArgs(Message message, Command command, int? userPermissions, string[] args)
{
Message = message;
Command = command;
UserPermissions = userPermissions;
Args = args;
}
}

public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput }
public class CommandErrorEventArgs : CommandEventArgs
{
public CommandErrorType ErrorType { get; }
public Exception Exception { get; }

public CommandErrorEventArgs(CommandErrorType errorType, CommandEventArgs baseArgs, Exception ex)
: base(baseArgs.Message, baseArgs.Command, baseArgs.UserPermissions, baseArgs.Args)
{
Exception = ex;
ErrorType = errorType;
}
}

public partial class CommandService
{
public event EventHandler<CommandEventArgs> RanCommand;
private void RaiseRanCommand(CommandEventArgs args)
{
if (RanCommand != null)
RanCommand(this, args);
}
public event EventHandler<CommandErrorEventArgs> CommandError;
private void RaiseCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null)
{
if (CommandError != null)
CommandError(this, new CommandErrorEventArgs(errorType, args, ex));
}
}
}

+ 184
- 0
src/Discord.Net.Commands/CommandService.cs View File

@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Commands
{
/// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary>
public partial class CommandService : IService
{
private DiscordClient _client;

CommandServiceConfig Config { get; }
public IEnumerable<Command> Commands => _commands;
private readonly List<Command> _commands;

internal CommandMap Map => _map;
private readonly CommandMap _map;

public CommandService(CommandServiceConfig config)
{
Config = config;
_commands = new List<Command>();
_map = new CommandMap(null);
}

void IService.Install(DiscordClient client)
{
_client = client;
Config.Lock();

if (Config.HelpMode != HelpMode.Disable)
{
CreateCommand("help")
.Parameter("command", ParameterType.Multiple)
.Hide()
.Info("Returns information about commands.")
.Do(async e =>
{
Channel channel = Config.HelpMode == HelpMode.Public ? e.Channel : await client.CreatePMChannel(e.User);
if (e.Args.Length > 0) //Show command help
{
var cmd = _map.GetCommand(string.Join(" ", e.Args));
if (cmd != null)
await ShowHelp(cmd, e.User, channel);
else
await client.SendMessage(channel, "Unable to display help: unknown command.");
}
else //Show general help
await ShowHelp(e.User, channel);
});
}

client.MessageReceived += async (s, e) =>
{
if (_commands.Count == 0) return;
if (e.Message.IsAuthor) return;

string msg = e.Message.Text;
if (msg.Length == 0) return;

//Check for command char if one is provided
var chars = Config.CommandChars;
if (chars.Length > 0)
{
if (!chars.Contains(msg[0]))
return;
msg = msg.Substring(1);
}

//Parse command
Command command;
int argPos;
CommandParser.ParseCommand(msg, _map, out command, out argPos);
if (command == null)
{
CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null);
RaiseCommandError(CommandErrorType.UnknownCommand, errorArgs);
return;
}
else
{
int userPermissions = Config.PermissionResolver?.Invoke(e.Message.User) ?? 0;

//Parse arguments
string[] args;
var error = CommandParser.ParseArgs(msg, argPos, command, out args);
if (error != null)
{
var errorArgs = new CommandEventArgs(e.Message, command, userPermissions, null);
RaiseCommandError(error.Value, errorArgs);
return;
}
var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args);

// Check permissions
if (userPermissions < command.MinPermissions)
{
RaiseCommandError(CommandErrorType.BadPermissions, eventArgs);
return;
}

// Run the command
try
{
RaiseRanCommand(eventArgs);
await command.Run(eventArgs).ConfigureAwait(false);
}
catch (Exception ex)
{
RaiseCommandError(CommandErrorType.Exception, eventArgs, ex);
}
}
};
}

public Task ShowHelp(User user, Channel channel)
{
int permissions = Config.PermissionResolver(user);

StringBuilder output = new StringBuilder();
output.AppendLine("These are the commands you can use:");
output.Append(string.Join(", ", _commands
.Where(x => permissions >= x.MinPermissions && !x.IsHidden)
.Select(x => '`' + x.Text + '`')));

var chars = Config.CommandChars;
if (chars.Length > 0)
{
if (chars.Length == 1)
output.AppendLine($"\nYou can use `{chars[0]}` to call a command.");
else
output.AppendLine($"\nYou can use `{string.Join(" ", chars.Take(chars.Length - 1))}` or `{chars.Last()}` to call a command.");
}

output.AppendLine("`help <command>` can tell you more about how to use a command.");

return _client.SendMessage(channel, output.ToString());
}
public Task ShowHelp(Command command, User user, Channel channel)
{
StringBuilder output = new StringBuilder();

output.Append($"`{command.Text}`");

if (command.MinArgs != null && command.MaxArgs != null)
{
if (command.MinArgs == command.MaxArgs)
{
if (command.MaxArgs != 0)
output.Append($" {command.MinArgs.ToString()} Args");
}
else
output.Append($" {command.MinArgs.ToString()} - {command.MaxArgs.ToString()} Args");
}
else if (command.MinArgs != null && command.MaxArgs == null)
output.Append($" ≥{command.MinArgs.ToString()} Args");
else if (command.MinArgs == null && command.MaxArgs != null)
output.Append($" ≤{command.MaxArgs.ToString()} Args");

output.Append($": {command.Description ?? "No description set for this command."}");

return _client.SendMessage(channel, output.ToString());
}

public void CreateCommandGroup(string cmd, Action<CommandGroupBuilder> config = null)
=> config(new CommandGroupBuilder(this, cmd, 0));
public CommandBuilder CreateCommand(string cmd)
{
var command = new Command(cmd);
_commands.Add(command);
return new CommandBuilder(this, command, "");
}

internal void AddCommand(Command command)
{
_commands.Add(command);
_map.AddCommand(command.Text, command);
}
}
}

+ 49
- 0
src/Discord.Net.Commands/CommandServiceConfig.cs View File

@@ -0,0 +1,49 @@
using System;

namespace Discord.Commands
{
public enum HelpMode
{
/// <summary> Disable the automatic help command. </summary>
Disable,
/// <summary> Use the automatic help command and respond in the channel the command is used. </summary>
Public,
/// <summary> Use the automatic help command and respond in a private message. </summary>
Private
}
public class CommandServiceConfig
{
public Func<User, int> PermissionResolver { get { return _permissionsResolver; } set { SetValue(ref _permissionsResolver, value); } }
private Func<User, int> _permissionsResolver;

public char? CommandChar
{
get
{
return _commandChars.Length > 0 ? _commandChars[0] : (char?)null;
}
set
{
if (value != null)
CommandChars = new char[] { value.Value };
else
CommandChars = new char[0];
}
}
public char[] CommandChars { get { return _commandChars; } set { SetValue(ref _commandChars, value); } }
private char[] _commandChars = new char[] { '!' };

public HelpMode HelpMode { get { return _helpMode; } set { SetValue(ref _helpMode, value); } }
private HelpMode _helpMode = HelpMode.Disable;

//Lock
protected bool _isLocked;
internal void Lock() { _isLocked = true; }
protected void SetValue<T>(ref T storage, T value)
{
if (_isLocked)
throw new InvalidOperationException("Unable to modify a discord client's configuration after it has been created.");
storage = value;
}
}
}

+ 0
- 60
src/Discord.Net.Commands/CommandsPlugin.Events.cs View File

@@ -1,60 +0,0 @@
using System;

namespace Discord.Commands
{
public class PermissionException : Exception { public PermissionException() : base("User does not have permission to run this command.") { } }
public class CommandEventArgs
{
public Message Message { get; }
public Command Command { get; }
public string CommandText { get; }
public string ArgText { get; }
public int? Permissions { get; }
public string[] Args { get; }
public User User => Message.User;
public Channel Channel => Message.Channel;
public Server Server => Message.Channel.Server;

public CommandEventArgs(Message message, Command command, string commandText, string argText, int? permissions, string[] args)
{
Message = message;
Command = command;
CommandText = commandText;
ArgText = argText;
Permissions = permissions;
Args = args;
}
}
public class CommandErrorEventArgs : CommandEventArgs
{
public Exception Exception { get; }

public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex)
: base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args)
{
Exception = ex;
}
}
public partial class CommandsPlugin
{
public event EventHandler<CommandEventArgs> RanCommand;
private void RaiseRanCommand(CommandEventArgs args)
{
if (RanCommand != null)
RanCommand(this, args);
}
public event EventHandler<CommandEventArgs> UnknownCommand;
private void RaiseUnknownCommand(CommandEventArgs args)
{
if (UnknownCommand != null)
UnknownCommand(this, args);
}
public event EventHandler<CommandErrorEventArgs> CommandError;
private void RaiseCommandError(CommandEventArgs args, Exception ex)
{
if (CommandError != null)
CommandError(this, new CommandErrorEventArgs(args, ex));
}
}
}

+ 0
- 138
src/Discord.Net.Commands/CommandsPlugin.cs View File

@@ -1,138 +0,0 @@
using System;
using System.Collections.Generic;

namespace Discord.Commands
{
/// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary>
public partial class CommandsPlugin
{
private readonly DiscordClient _client;
private List<Command> _commands;
private Func<User, int> _getPermissions;

public IEnumerable<Command> Commands => _commands;

public char CommandChar { get; set; }
public bool UseCommandChar { get; set; }
public bool RequireCommandCharInPublic { get; set; }
public bool RequireCommandCharInPrivate { get; set; }

public CommandsPlugin(DiscordClient client, Func<User, int> getPermissions = null)
{
_client = client;
_getPermissions = getPermissions;
_commands = new List<Command>();

CommandChar = '/';
UseCommandChar = false;
RequireCommandCharInPublic = true;
RequireCommandCharInPrivate = true;

client.MessageReceived += async (s, e) =>
{
//If commands aren't being used, don't bother processing them
if (_commands.Count == 0)
return;

//Ignore messages from ourselves
if (e.Message.User == client.CurrentUser)
return;

//Check for the command character
string msg = e.Message.Text;
if (UseCommandChar)
{
if (msg.Length == 0)
return;
bool isPrivate = e.Message.Channel.IsPrivate;
bool hasCommandChar = msg[0] == CommandChar;
if (hasCommandChar)
msg = msg.Substring(1);
if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar)
return;
if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar)
return;
}

CommandPart[] args;
if (!CommandParser.ParseArgs(msg, out args))
return;

for (int i = 0; i < _commands.Count; i++)
{
Command cmd = _commands[i];

//Check Command Parts
if (args.Length < cmd.Parts.Length)
continue;

bool isValid = true;
for (int j = 0; j < cmd.Parts.Length; j++)
{
if (!string.Equals(args[j].Value, cmd.Parts[j], StringComparison.OrdinalIgnoreCase))
{
isValid = false;
break;
}
}
if (!isValid)
continue;

//Check Arg Count
int argCount = args.Length - cmd.Parts.Length;
if (argCount < cmd.MinArgs || argCount > cmd.MaxArgs)
continue;

//Clean Args
string[] newArgs = new string[argCount];
for (int j = 0; j < newArgs.Length; j++)
newArgs[j] = args[j + cmd.Parts.Length].Value;

//Get ArgText
string argText;
if (argCount == 0)
argText = "";
else
argText = msg.Substring(args[cmd.Parts.Length].Index);

//Check Permissions
int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0;
var eventArgs = new CommandEventArgs(e.Message, cmd, msg, argText, permissions, newArgs);
if (permissions < cmd.MinPerms)
{
RaiseCommandError(eventArgs, new PermissionException());
return;
}

//Run Command
RaiseRanCommand(eventArgs);
try
{
var task = cmd.Handler(eventArgs);
if (task != null)
await task.ConfigureAwait(false);
}
catch (Exception ex)
{
RaiseCommandError(eventArgs, ex);
}
break;
}
};
}

public void CreateCommandGroup(string cmd, Action<CommandGroupBuilder> config = null)
=> config(new CommandGroupBuilder(this, cmd, 0));
public CommandBuilder CreateCommand(string cmd)
{
var command = new Command(cmd);
_commands.Add(command);
return new CommandBuilder(command);
}

internal void AddCommand(Command command)
{
_commands.Add(command);
}
}
}

+ 5
- 2
src/Discord.Net.Net45/Discord.Net.csproj View File

@@ -88,8 +88,8 @@
<Compile Include="..\Discord.Net\API\Enums\PermissionTarget.cs">
<Link>API\Enums\PermissionTarget.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Enums\Regions.cs">
<Link>API\Enums\Regions.cs</Link>
<Compile Include="..\Discord.Net\API\Enums\Region.cs">
<Link>API\Enums\Region.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Enums\StringEnum.cs">
<Link>API\Enums\StringEnum.cs</Link>
@@ -232,6 +232,9 @@
<Compile Include="..\Discord.Net\HttpException.cs">
<Link>HttpException.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\IService.cs">
<Link>IService.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Models\Channel.cs">
<Link>Models\Channel.cs</Link>
</Compile>


+ 6
- 0
src/Discord.Net/API/Enums/ChannelType.cs View File

@@ -24,5 +24,11 @@
return new ChannelType(value);
}
}

public static implicit operator ChannelType(string value) => FromString(value);
public static bool operator ==(ChannelType a, ChannelType b) => a?._value == b?._value;
public static bool operator !=(ChannelType a, ChannelType b) => a?._value != b?._value;
public override bool Equals(object obj) => (obj as ChannelType)?._value == _value;
public override int GetHashCode() => _value.GetHashCode();
}
}

+ 6
- 0
src/Discord.Net/API/Enums/PermissionTarget.cs View File

@@ -24,5 +24,11 @@
return new PermissionTarget(value);
}
}

public static implicit operator PermissionTarget(string value) => FromString(value);
public static bool operator ==(PermissionTarget a, PermissionTarget b) => a?._value == b?._value;
public static bool operator !=(PermissionTarget a, PermissionTarget b) => a?._value != b?._value;
public override bool Equals(object obj) => (obj as PermissionTarget)?._value == _value;
public override int GetHashCode() => _value.GetHashCode();
}
}

src/Discord.Net/API/Enums/Regions.cs → src/Discord.Net/API/Enums/Region.cs View File

@@ -34,5 +34,11 @@
return new Region(value);
}
}

public static implicit operator Region(string value) => FromString(value);
public static bool operator ==(Region a, Region b) => a?._value == b?._value;
public static bool operator !=(Region a, Region b) => a?._value != b?._value;
public override bool Equals(object obj) => (obj as Region)?._value == _value;
public override int GetHashCode() => _value.GetHashCode();
}
}

+ 1
- 39
src/Discord.Net/API/Enums/StringEnum.cs View File

@@ -2,7 +2,7 @@
{
public abstract class StringEnum
{
private string _value;
protected string _value;
protected StringEnum(string value)
{
_value = value;
@@ -10,43 +10,5 @@

public string Value => _value;
public override string ToString() => _value;

public override bool Equals(object obj)
{
var enum2 = obj as StringEnum;
if (enum2 == (StringEnum)null)
return false;
else
return _value == enum2._value;
}
public override int GetHashCode()
{
return _value.GetHashCode();
}

public static bool operator ==(StringEnum a, StringEnum b)
{
return a?._value == b?._value;
}
public static bool operator !=(StringEnum a, StringEnum b)
{
return a?._value != b?._value;
}
public static bool operator ==(StringEnum a, string b)
{
return a?._value == b;
}
public static bool operator !=(StringEnum a, string b)
{
return a?._value != b;
}
public static bool operator ==(string a, StringEnum b)
{
return a == b?._value;
}
public static bool operator !=(string a, StringEnum b)
{
return a != b?._value;
}
}
}

+ 6
- 0
src/Discord.Net/API/Enums/UserStatus.cs View File

@@ -28,5 +28,11 @@
return new UserStatus(value);
}
}

public static implicit operator UserStatus(string value) => FromString(value);
public static bool operator ==(UserStatus a, UserStatus b) => a?._value == b?._value;
public static bool operator !=(UserStatus a, UserStatus b) => a?._value != b?._value;
public override bool Equals(object obj) => (obj as UserStatus)?._value == _value;
public override int GetHashCode() => _value.GetHashCode();
}
}

+ 20
- 16
src/Discord.Net/DiscordClient.Channels.cs View File

@@ -84,26 +84,30 @@ namespace Discord
public IEnumerable<Channel> FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();
var query = server.Channels.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));

IEnumerable<Channel> result;
if (!exactMatch && name.StartsWith("#"))
{
string name2 = name.Substring(1);
result = _channels.Where(x => x.Server == server &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
if (!exactMatch && name.Length >= 2)
{
result = _channels.Where(x => x.Server == server &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention
{
string id = name.Substring(2, name.Length - 3);
var channel = _channels[id];
if (channel != null)
query = query.Concat(new Channel[] { channel });
}
else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention
{
string name2 = name.Substring(1);
query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
}
}

if (type != (string)null)
result = result.Where(x => x.Type == type);

return result;
if (type != null)
query = query.Where(x => x.Type == type);
return query;
}

/// <summary> Creates a new channel with the provided name and type. </summary>
@@ -111,7 +115,7 @@ namespace Discord
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
if (type == (string)null) throw new ArgumentNullException(nameof(type));
if (type == null) throw new ArgumentNullException(nameof(type));
CheckReady();

var response = await _api.CreateChannel(server.Id, name, type.Value).ConfigureAwait(false);


+ 24
- 12
src/Discord.Net/DiscordClient.Messages.cs View File

@@ -93,7 +93,6 @@ namespace Discord
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();

return SendMessage(channel, text, false);
@@ -103,7 +102,6 @@ namespace Discord
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();

return SendMessage(channel, text, false);
@@ -113,7 +111,6 @@ namespace Discord
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (text == null) throw new ArgumentNullException(nameof(text));
if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();

var channel = await CreatePMChannel(user).ConfigureAwait(false);
@@ -122,7 +119,8 @@ namespace Discord
private async Task<Message> SendMessage(Channel channel, string text, bool isTextToSpeech)
{
Message msg;
var userIds = !channel.IsPrivate ? Mention.GetUserIds(text).Distinct() : new string[0];
var server = channel.Server;

if (Config.UseMessageQueue)
{
var nonce = GenerateNonce();
@@ -136,14 +134,24 @@ namespace Discord
ChannelId = channel.Id,
IsTextToSpeech = isTextToSpeech
});
msg.Mentions = userIds.Select(x => _users[x, channel.Server.Id]).Where(x => x != null).ToArray();
msg.IsQueued = true;
msg.Nonce = nonce;
msg.IsQueued = true;

if (text.Length > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");

_pendingMessages.Enqueue(msg);
}
else
{
var model = await _api.SendMessage(channel.Id, text, userIds, null, isTextToSpeech).ConfigureAwait(false);
var mentionedUsers = new List<User>();
if (!channel.IsPrivate)
text = Mention.CleanUserMentions(this, server, text, mentionedUsers);

if (text.Length > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");

var model = await _api.SendMessage(channel.Id, text, mentionedUsers.Select(x => x.Id), null, isTextToSpeech).ConfigureAwait(false);
msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
@@ -168,13 +176,17 @@ namespace Discord
{
if (message == null) throw new ArgumentNullException(nameof(message));
if (text == null) throw new ArgumentNullException(nameof(text));
if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();

if (text != null && text.Length > MaxMessageSize)
text = text.Substring(0, MaxMessageSize);
var channel = message.Channel;
var mentionedUsers = new List<User>();
if (!channel.IsPrivate)
text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers);

if (text.Length > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");

return _api.EditMessage(message.Id, message.Channel.Id, text, Mention.GetUserIds(text));
return _api.EditMessage(message.Id, message.Channel.Id, text, mentionedUsers.Select(x => x.Id));
}

/// <summary> Deletes the provided message. </summary>
@@ -258,7 +270,7 @@ namespace Discord
SendMessageResponse response = null;
try
{
response = await _api.SendMessage(msg.Channel.Id, msg.RawText, msg.Mentions.Select(x => x.Id), msg.Nonce, msg.IsTTS).ConfigureAwait(false);
response = await _api.SendMessage(msg.Channel.Id, msg.RawText, msg.MentionedUsers.Select(x => x.Id), msg.Nonce, msg.IsTTS).ConfigureAwait(false);
}
catch (WebException) { break; }
catch (HttpException) { hasFailed = true; }


+ 1
- 1
src/Discord.Net/DiscordClient.Servers.cs View File

@@ -84,7 +84,7 @@ namespace Discord
public async Task<Server> CreateServer(string name, Region region)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (region == (string)null) throw new ArgumentNullException(nameof(region));
if (region == null) throw new ArgumentNullException(nameof(region));
CheckReady();

var response = await _api.CreateServer(name, region.Value).ConfigureAwait(false);


+ 33
- 10
src/Discord.Net/DiscordClient.Users.cs View File

@@ -141,7 +141,7 @@ namespace Discord
return _users[user?.Id, server.Id];
}

/// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary>
/// <summary> Returns all users with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<User> FindUsers(Server server, string name, string discriminator = null, bool exactMatch = false)
{
@@ -149,16 +149,39 @@ namespace Discord
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

IEnumerable<User> query;
if (!exactMatch && name.StartsWith("@"))
return FindUsers(server.Members, server.Id, name, discriminator, exactMatch);
}
/// <summary> Returns all users with the specified channel and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<User> FindUsers(Channel channel, string name, string discriminator = null, bool exactMatch = false)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

return FindUsers(channel.Members, channel.IsPrivate ? null : channel.Server.Id, name, discriminator, exactMatch);
}

private IEnumerable<User> FindUsers(IEnumerable<User> users, string serverId, string name, string discriminator = null, bool exactMatch = false)
{
var query = users.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));

if (!exactMatch && name.Length >= 2)
{
string name2 = name.Substring(1);
query = server.Members.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention
{
string id = name.Substring(2, name.Length - 3);
var channel = _users[id, serverId];
if (channel != null)
query = query.Concat(new User[] { channel });
}
else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention
{
string name2 = name.Substring(1);
query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
}
}
else
query = server.Members.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));

if (discriminator != null)
query = query.Where(x => x.Discriminator == discriminator);
return query;
@@ -227,7 +250,7 @@ namespace Discord

public Task SetStatus(UserStatus status)
{
if (status == (string)null) throw new ArgumentNullException(nameof(status));
if (status == null) throw new ArgumentNullException(nameof(status));
if (status != UserStatus.Online && status != UserStatus.Idle)
throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status));
CheckReady();


+ 20
- 3
src/Discord.Net/DiscordClient.cs View File

@@ -17,6 +17,7 @@ namespace Discord
private readonly JsonSerializer _serializer;
private readonly ConcurrentQueue<Message> _pendingMessages;
private readonly ConcurrentDictionary<string, DiscordWSClient> _voiceClients;
private readonly Dictionary<Type, IService> _services;
private bool _sentInitialLog;
private uint _nextVoiceClientId;
private UserStatus _status;
@@ -46,6 +47,7 @@ namespace Discord
_roles = new Roles(this, cacheLock);
_servers = new Servers(this, cacheLock);
_globalUsers = new GlobalUsers(this, cacheLock);
_services = new Dictionary<Type, IService>();

_status = UserStatus.Online;

@@ -168,7 +170,6 @@ namespace Discord
_serializer.MissingMemberHandling = MissingMemberHandling.Error;
#endif
}

internal override VoiceWebSocket CreateVoiceSocket()
{
var socket = base.CreateVoiceSocket();
@@ -216,7 +217,6 @@ namespace Discord
await Connect(token).ConfigureAwait(false);
return token;
}

/// <summary> Connects to the Discord server with the provided token. </summary>
public async Task Connect(string token)
{
@@ -273,6 +273,22 @@ namespace Discord
_currentUser = null;
}

public void AddService<T>(T obj)
where T : class, IService
{
_services.Add(typeof(T), obj);
obj.Install(this);
}
public T GetService<T>()
where T : class, IService
{
IService service;
if (_services.TryGetValue(typeof(T), out service))
return service as T;
else
return null;
}

protected override IEnumerable<Task> GetTasks()
{
if (Config.UseMessageQueue)
@@ -294,7 +310,8 @@ namespace Discord
var data = e.Payload.ToObject<ReadyEvent>(_serializer);
_currentUser = _users.GetOrAdd(data.User.Id, null);
_currentUser.Update(data.User);
foreach (var model in data.Guilds)
_currentUser.GlobalUser.Update(data.User);
foreach (var model in data.Guilds)
{
if (!model.Unavailable)
{


+ 28
- 19
src/Discord.Net/Helpers/Mention.cs View File

@@ -8,6 +8,7 @@ namespace Discord
{
private static readonly Regex _userRegex = new Regex(@"<@([0-9]+?)>", RegexOptions.Compiled);
private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+?)>", RegexOptions.Compiled);
private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled);

/// <summary> Returns the string used to create a user mention. </summary>
public static string User(User user)
@@ -15,42 +16,50 @@ namespace Discord
/// <summary> Returns the string used to create a channel mention. </summary>
public static string Channel(Channel channel)
=> $"<#{channel.Id}>";
/// <summary> Returns the string used to create a channel mention. </summary>
/// <summary> Returns the string used to create a mention to everyone in a channel. </summary>
public static string Everyone()
=> $"@everyone";

internal static string ConvertToNames(DiscordClient client, Server server, string text)
internal static string CleanUserMentions(DiscordClient client, Server server, string text, List<User> users = null)
{
text = _userRegex.Replace(text, new MatchEvaluator(e =>
return _userRegex.Replace(text, new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var user = client.Users[id, server?.Id];
if (user != null)
{
if (users != null)
users.Add(user);
return '@' + user.Name;
}
else //User not found
return '@' + e.Value;
}));
if (server != null)
}
internal static string CleanChannelMentions(DiscordClient client, Server server, string text, List<Channel> channels = null)
{
return _channelRegex.Replace(text, new MatchEvaluator(e =>
{
text = _channelRegex.Replace(text, new MatchEvaluator(e =>
string id = e.Value.Substring(2, e.Value.Length - 3);
var channel = client.Channels[id];
if (channel != null && channel.Server.Id == server.Id)
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var channel = client.Channels[id];
if (channel != null && channel.Server.Id == server.Id)
return '#' + channel.Name;
else //Channel not found
if (channels != null)
channels.Add(channel);
return '#' + channel.Name;
}
else //Channel not found
return '#' + e.Value;
}));
}
return text;
}));
}

internal static IEnumerable<string> GetUserIds(string text)
internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List<Role> roles = null)
{
return _userRegex.Matches(text)
.OfType<Match>()
.Select(x => x.Groups[1].Value)
.Where(x => x != null);
return _roleRegex.Replace(text, new MatchEvaluator(e =>
{
if (roles != null && user.GetPermissions(channel).MentionEveryone)
roles.Add(channel.Server.EveryoneRole);
return e.Value;
}));
}
}
}

+ 7
- 0
src/Discord.Net/IService.cs View File

@@ -0,0 +1,7 @@
namespace Discord
{
public interface IService
{
void Install(DiscordClient client);
}
}

+ 7
- 7
src/Discord.Net/Models/Channel.cs View File

@@ -146,14 +146,12 @@ namespace Discord
var cacheLength = _client.Config.MessageCacheLength;
if (cacheLength > 0)
{
while (_messages.Count > cacheLength - 1)
var oldestIds = _messages.Select(x => x.Value.Id).OrderBy(x => x).Take(_messages.Count - cacheLength);
foreach (var id in oldestIds)
{
var oldest = _messages.Select(x => x.Value.Id).OrderBy(x => x).FirstOrDefault();
if (oldest != null)
{
if (_messages.TryRemove(oldest, out message))
_client.Messages.TryRemove(oldest);
}
Message removed;
if (_messages.TryRemove(id, out removed))
_client.Messages.TryRemove(id);
}
_messages.TryAdd(message.Id, message);
}
@@ -207,6 +205,8 @@ namespace Discord
user.UpdateChannelPermissions(this);
}

public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}

+ 4
- 0
src/Discord.Net/Models/Color.cs View File

@@ -77,5 +77,9 @@ namespace Discord
uint mask = (uint)~(0xFF << bit);
_rawValue = (_rawValue & mask) | ((uint)value << bit);
}

public override bool Equals(object obj) => obj is Color && (obj as Color)._rawValue == _rawValue;
public override int GetHashCode() => _rawValue.GetHashCode();
public override string ToString() => '#' + _rawValue.ToString("X");
}
}

+ 2
- 0
src/Discord.Net/Models/GlobalUser.cs View File

@@ -69,6 +69,8 @@ namespace Discord
_client.GlobalUsers.TryRemove(Id);
}

public override bool Equals(object obj) => obj is GlobalUser && (obj as GlobalUser).Id == Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Id;
}
}

+ 66
- 63
src/Discord.Net/Models/Invite.cs View File

@@ -5,7 +5,62 @@ using Newtonsoft.Json;
namespace Discord
{
public sealed class Invite : CachedObject
{
{
public sealed class ServerInfo
{
/// <summary> Returns the unique identifier of this server. </summary>
public string Id { get; }
/// <summary> Returns the name of this server. </summary>
public string Name { get; }

internal ServerInfo(string id, string name)
{
Id = id;
Name = name;
}
}
public sealed class ChannelInfo
{
/// <summary> Returns the unique identifier of this channel. </summary>
public string Id { get; }
/// <summary> Returns the name of this channel. </summary>
public string Name { get; }

internal ChannelInfo(string id, string name)
{
Id = id;
Name = name;
}
}
public sealed class InviterInfo
{
/// <summary> Returns the unique identifier for this user. </summary>
public string Id { get; }
/// <summary> Returns the name of this user. </summary>
public string Name { get; }
/// <summary> Returns the by-name unique identifier for this user. </summary>
public string Discriminator { get; }
/// <summary> Returns the unique identifier for this user's avatar. </summary>
public string AvatarId { get; }
/// <summary> Returns the full path to this user's avatar. </summary>
public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId);

internal InviterInfo(string id, string name, string discriminator, string avatarId)
{
Id = id;
Name = name;
Discriminator = discriminator;
AvatarId = avatarId;
}
}

/// <summary> Returns information about the server this invite is attached to. </summary>
public ServerInfo Server { get; private set; }
/// <summary> Returns information about the channel this invite is attached to. </summary>
public ChannelInfo Channel { get; private set; }
/// <summary> Returns information about the user that created this invite. </summary>
public InviterInfo Inviter { get; private set; }

/// <summary> Returns, if enabled, an alternative human-readable code for URLs. </summary>
public string XkcdCode { get; }
/// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary>
@@ -23,77 +78,23 @@ namespace Discord
/// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary>
public string Url => API.Endpoints.InviteUrl(XkcdCode ?? Id);

/// <summary> Returns the user that created this invite. </summary>
[JsonIgnore]
public User Inviter => _inviter.Value;
private readonly Reference<User> _inviter;
private User _generatedInviter;

/// <summary> Returns the server this invite is to. </summary>
[JsonIgnore]
public Server Server => _server.Value;
private readonly Reference<Server> _server;
private Server _generatedServer;

/// <summary> Returns the channel this invite is to. </summary>
[JsonIgnore]
public Channel Channel => _channel.Value;
private readonly Reference<Channel> _channel;
private Channel _generatedChannel;

internal Invite(DiscordClient client, string code, string xkcdPass, string serverId, string inviterId, string channelId)
: base(client, code)
{
XkcdCode = xkcdPass;
_server = new Reference<Server>(serverId, x =>
{
var server = _client.Servers[x];
if (server == null)
{
server = _generatedServer = new Server(client, x);
server.Cache();
}
return server;
});
_inviter = new Reference<User>(serverId, x =>
{
var inviter = _client.Users[x, _server.Id];
if (inviter == null)
{
inviter = _generatedInviter = new User(client, x, _server.Id);
inviter.Cache();
}
return inviter;
});
_channel = new Reference<Channel>(serverId, x =>
{
var channel = _client.Channels[x];
if (channel == null)
{
channel = _generatedChannel = new Channel(client, x, _server.Id, null);
channel.Cache();
}
return channel;
});
}
internal override void LoadReferences()
{
_server.Load();
_inviter.Load();
_channel.Load();
}
internal override void LoadReferences() { }
internal override void UnloadReferences() { }


internal void Update(InviteReference model)
{
if (model.Guild != null && _generatedServer != null)
_generatedServer.Update(model.Guild);
if (model.Inviter != null && _generatedInviter != null)
_generatedInviter.Update(model.Inviter);
if (model.Channel != null && _generatedChannel != null)
_generatedChannel.Update(model.Channel);
}
if (model.Guild != null)
Server = new ServerInfo(model.Guild.Id, model.Guild.Name);
if (model.Channel != null)
Channel = new ChannelInfo(model.Channel.Id, model.Channel.Name);
if (model.Inviter != null)
Inviter = new InviterInfo(model.Inviter.Id, model.Inviter.Username, model.Inviter.Discriminator, model.Inviter.Avatar);
}
internal void Update(InviteInfo model)
{
Update(model as InviteReference);
@@ -112,6 +113,8 @@ namespace Discord
CreatedAt = model.CreatedAt.Value;
}

public override bool Equals(object obj) => obj is Invite && (obj as Invite).Id == Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => XkcdCode ?? Id;
}
}

+ 50
- 17
src/Discord.Net/Models/Message.cs View File

@@ -90,17 +90,13 @@ namespace Discord
Height = height;
}
}

private string _cleanText;
/// <summary> Returns the local unique identifier for this message. </summary>
public string Nonce { get; internal set; }

/// <summary> Returns true if the logged-in user was mentioned. </summary>
/// <remarks> This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone). </remarks>
public bool IsMentioningMe { get; private set; }
/// <summary> Returns true if @everyone was mentioned by someone with permissions to do so. </summary>
public bool IsMentioningEveryone { get; private set; }
/// <summary> Returns true if the message was sent as text-to-speech by someone with permissions to do so. </summary>
public bool IsTTS { get; private set; }
/// <summary> Returns true if the message is still in the outgoing message queue. </summary>
@@ -110,8 +106,7 @@ namespace Discord
/// <summary> Returns the raw content of this message as it was received from the server.. </summary>
public string RawText { get; private set; }
/// <summary> Returns the content of this message with any special references such as mentions converted. </summary>
/// <remarks> This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache. </remarks>
public string Text => _cleanText != null ? _cleanText : (_cleanText = Mention.ConvertToNames(_client, Server, RawText));
public string Text { get; private set; }
/// <summary> Returns the timestamp for when this message was sent. </summary>
public DateTime Timestamp { get; private set; }
/// <summary> Returns the timestamp for when this message was last edited. </summary>
@@ -125,8 +120,16 @@ namespace Discord
/// <summary> Returns a collection of all users mentioned in this message. </summary>
[JsonIgnore]
public IEnumerable<User> Mentions { get; internal set; }
public IEnumerable<User> MentionedUsers { get; internal set; }

/// <summary> Returns a collection of all channels mentioned in this message. </summary>
[JsonIgnore]
public IEnumerable<Channel> MentionedChannels { get; internal set; }

/// <summary> Returns a collection of all roles mentioned in this message. </summary>
[JsonIgnore]
public IEnumerable<Role> MentionedRoles { get; internal set; }

/// <summary> Returns the server containing the channel this message was sent to. </summary>
[JsonIgnore]
public Server Server => _channel.Value.Server;
@@ -174,6 +177,8 @@ namespace Discord

internal void Update(MessageInfo model)
{
var channel = Channel;
var server = channel.Server;
if (model.Attachments != null)
{
Attachments = model.Attachments
@@ -197,9 +202,7 @@ namespace Discord
return new Embed(x.Url, x.Type, x.Title, x.Description, author, provider, thumbnail);
}).ToArray();
}

if (model.IsMentioningEveryone != null)
IsMentioningEveryone = model.IsMentioningEveryone.Value;
if (model.IsTextToSpeech != null)
IsTTS = model.IsTextToSpeech.Value;
if (model.Timestamp != null)
@@ -208,16 +211,46 @@ namespace Discord
EditedTimestamp = model.EditedTimestamp;
if (model.Mentions != null)
{
Mentions = model.Mentions.Select(x => _client.Users[x.Id, Channel.Server?.Id]).ToArray();
IsMentioningMe = model.Mentions.Any(x => x.Id == _client.CurrentUserId);
MentionedUsers = model.Mentions
.Select(x => _client.Users[x.Id, Channel.Server?.Id])
.ToArray();
}
if (model.Content != null)
{
RawText = model.Content;
_cleanText = null;
string text = model.Content;
RawText = text;

//var mentionedUsers = new List<User>();
var mentionedChannels = new List<Channel>();
var mentionedRoles = new List<Role>();
text = Mention.CleanUserMentions(_client, server, text/*, mentionedUsers*/);
if (server != null)
{
text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels);
text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles);
}
Text = text;

//MentionedUsers = mentionedUsers;
MentionedChannels = mentionedChannels;
MentionedRoles = mentionedRoles;
}
}

if (server != null)
{
var me = server.CurrentUser;
IsMentioningMe = (MentionedUsers?.Contains(me) ?? false) ||
(MentionedRoles?.Any(x => me.HasRole(x)) ?? false);
}
else
{
var me = _client.CurrentUser;
IsMentioningMe = MentionedUsers?.Contains(me) ?? false;
}
}

public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => $"{User}: {RawText}";
}
}

+ 8
- 0
src/Discord.Net/Models/Permissions.cs View File

@@ -137,6 +137,9 @@ namespace Discord
if (_isLocked)
throw new InvalidOperationException("Unable to edit cached permissions directly, use Copy() to make an editable copy.");
}

public override bool Equals(object obj) => obj is Permissions && (obj as Permissions)._rawValue == _rawValue;
public override int GetHashCode() => _rawValue.GetHashCode();
}

public sealed class DualChannelPermissions
@@ -214,5 +217,10 @@ namespace Discord
Deny.SetBit(pos, false);
}
}

public override bool Equals(object obj) => obj is DualChannelPermissions &&
(obj as DualChannelPermissions).Allow.Equals(Allow) &&
(obj as DualChannelPermissions).Deny.Equals(Deny);
public override int GetHashCode() => unchecked(Allow.GetHashCode() + Deny.GetHashCode());
}
}

+ 2
- 0
src/Discord.Net/Models/Role.cs View File

@@ -70,6 +70,8 @@ namespace Discord
member.UpdateServerPermissions();
}

public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}

+ 2
- 0
src/Discord.Net/Models/Server.cs View File

@@ -252,6 +252,8 @@ namespace Discord
}
}

public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}

+ 4
- 1
src/Discord.Net/Models/User.cs View File

@@ -10,6 +10,7 @@ namespace Discord
public class User : CachedObject
{
internal static string GetId(string userId, string serverId) => (serverId ?? "Private") + '_' + userId;
internal static string GetAvatarUrl(string userId, string avatarId) => avatarId != null ? Endpoints.UserAvatar(userId, avatarId) : null;

private ConcurrentDictionary<string, Channel> _channels;
private ConcurrentDictionary<string, ChannelPermissions> _permissions;
@@ -24,7 +25,7 @@ namespace Discord
/// <summary> Returns the unique identifier for this user's current avatar. </summary>
public string AvatarId { get; private set; }
/// <summary> Returns the URL to this user's current avatar. </summary>
public string AvatarUrl => AvatarId != null ? API.Endpoints.UserAvatar(Id, AvatarId) : null;
public string AvatarUrl => GetAvatarUrl(Id, AvatarId);
/// <summary> Returns the datetime that this user joined this server. </summary>
public DateTime JoinedAt { get; private set; }

@@ -370,6 +371,8 @@ namespace Discord
return _roles.ContainsKey(role.Id);
}

public override bool Equals(object obj) => obj is User && (obj as User).Id == Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}

+ 3
- 7
src/Discord.Net/Net/Rest/SharpRestEngine.cs View File

@@ -52,19 +52,15 @@ namespace Discord.Net.Rest
}
private async Task<string> Send(RestRequest request, CancellationToken cancelToken)
{
bool hasRetried = false;
int retryCount = 0;
while (true)
{
var response = await _client.ExecuteTaskAsync(request, cancelToken).ConfigureAwait(false);
int statusCode = (int)response.StatusCode;
if (statusCode == 0) //Internal Error
{
if (!hasRetried)
{
//SSL/TTS Error seems to work if we immediately retry
hasRetried = true;
continue;
}
if (response.ErrorException.HResult == -2146233079 && retryCount++ < 5) //The request was aborted: Could not create SSL/TLS secure channel.
continue; //Seems to work if we immediately retry
throw response.ErrorException;
}
if (statusCode < 200 || statusCode >= 300) //2xx = Success


Loading…
Cancel
Save