Finite Reality 8 years ago
parent
commit
619e884506
19 changed files with 293 additions and 59 deletions
  1. +1
    -1
      docs/api/.manifest
  2. +40
    -1
      docs/guides/commands.md
  3. +28
    -0
      docs/guides/events.md
  4. +2
    -2
      docs/guides/samples.md
  5. +15
    -0
      docs/guides/samples/joining_audio.cs
  6. +14
    -0
      docs/guides/samples/typereader.cs
  7. +6
    -2
      docs/guides/toc.yml
  8. +28
    -0
      docs/guides/voice.md
  9. +8
    -2
      src/Discord.Net/API/DiscordRestApiClient.cs
  10. +2
    -0
      src/Discord.Net/Entities/Channels/IMessageChannel.cs
  11. +5
    -0
      src/Discord.Net/Rest/Entities/Channels/DMChannel.cs
  12. +5
    -0
      src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs
  13. +6
    -1
      src/Discord.Net/Rest/Entities/Channels/TextChannel.cs
  14. +9
    -11
      src/Discord.Net/Rpc/DiscordRpcClient.cs
  15. +15
    -13
      src/Discord.Net/WebSocket/DiscordSocketClient.cs
  16. +3
    -5
      src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs
  17. +104
    -19
      src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs
  18. +1
    -1
      src/Discord.Net/WebSocket/Extensions/ChannelExtensions.cs
  19. +1
    -1
      src/Discord.Net/WebSocket/Extensions/GuildExtensions.cs

+ 1
- 1
docs/api/.manifest
File diff suppressed because it is too large
View File


+ 40
- 1
docs/guides/commands.md View File

@@ -59,4 +59,43 @@ In the constructor of your module, any parameters will be filled in by the @Disc
>[!NOTE]
>If you accept `CommandService` or `IDependencyMap` as a parameter in your constructor, these parameters will be filled by the CommandService the module was loaded from, and the DependencyMap passed into it, respectively.

[!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)]
[!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)]

## Type Readers

Type Readers allow you to parse different types of arguments in your commands.

By default, the following Types are supported arguments:

- string
- sbyte/byte
- ushort/short
- uint/int
- ulong/long
- float, double, decimal
- DateTime/DateTimeOffset
- IUser/IGuildUser
- IChannel/IGuildChannel/ITextChannel/IVoiceChannel/IGroupChannel
- IRole
- IMessage

### Creating a Type Readers

To create a TypeReader, create a new class that imports @Discord and @Discord.Commands . Ensure your class inherits from @Discord.Commands.TypeReader

Next, satisfy the `TypeReader` class by overriding `Task<TypeReaderResult> Read(IMessage context, string input)`.

>[!NOTE]
>In many cases, Visual Stuido can fill this in for you, using the "Implement Abstract Class" IntelliSense hint.

Inside this task, add whatever logic you need to parse the input string.

Finally, return a `TypeReaderResult`. If you were able to successfully parse the input, return `TypeReaderResult.FromSuccess(parsedInput)`. Otherwise, return `TypeReaderResult.FromError`.

#### Sample

[!code-csharp[TypeReaders](samples/typereader.cs)]

### Installing TypeReaders

TypeReaders are not automatically discovered by the Command Service, and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader](xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_).

+ 28
- 0
docs/guides/events.md View File

@@ -0,0 +1,28 @@
---
title: Events
---

# Events

Messages from Discord are exposed via events, and follow a pattern of `Func<[event params], Task>`, which allows you to easily create either async or sync event handlers.

To hook into events, you must be using the @Discord.WebSocket.DiscordSocketClient, which provides WebSocket capabilities, necessary for receiving events.

>[!NOTE]
>The gateway will wait for all registered handlers of an event to finish before raising the next event. As a result of this, it is reccomended that if you need to perform any heavy work in an event handler, it is done on its own thread or Task.

**For further documentation of all events**, it is reccomended to look at the [Events Section](xref:Discord.WebSocket.DiscordSocketClient#events) on the API documentation of @Discord.WebSocket.DiscordSocketClient

## Connection State

Connection Events will be raised when the Connection State of your client changes.

[DiscordSocketClient.Connected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Connected) and [Disconnected](Discord_WebSocket_DiscordSocketClient_Disconnected) are raised when the Gateway Socket connects or disconnects, respectively.

>[!WARNING]
>You should not use DiscordClient.Connected to run code when your client first connects to Discord. The client has not received and parsed the READY event and guild stream yet, and will have an incomplete or empty cache.

[DiscordSocketClient.Ready](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Ready) is raised when the `READY` packet is parsed and received from Discord.

>[!NOTE]
>The [DiscordSocketClient.ConnectAsync](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_ConnectAsync_System_Boolean_) method will not return until the READY packet has been processed. By default, it also will not return until the guild stream has finished. This means it is safe to run bot code directly after awaiting the ConnectAsync method.

docs/guides/faq.md → docs/guides/samples.md View File

@@ -1,8 +1,8 @@
---
title: Frequently Asked Questions
title: Samples
---

# Frequently Asked Questions
# Samples

>[!NOTE]
>All of these samples assume you have `_client` defined as a `DiscordSocketClient`.

+ 15
- 0
docs/guides/samples/joining_audio.cs View File

@@ -0,0 +1,15 @@
// Create an IAudioClient, and store it for later use
private IAudioClient _audio;

// Create a Join command, that will join the parameter or the user's current voice channel
[Command("join")]
public async Task JoinChannel(IMessage msg,
IVoiceChannel channel = null)
{
// Get the audio channel
channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel;
if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; }

// Get the IAudioClient by calling the JoinAsync method
_audio = await channel.JoinAsync();
}

+ 14
- 0
docs/guides/samples/typereader.cs View File

@@ -0,0 +1,14 @@
using Discord;
using Discord.Commands;

public class BooleanTypeReader : TypeReader
{
public override Task<TypeReaderResult> Read(IMessage context, string input)
{
bool result;
if (bool.TryParse(input, out result))
return Task.FromResult(TypeReaderResult.FromSuccess(result));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input could not be parsed as a boolean."))
}
}

+ 6
- 2
docs/guides/toc.yml View File

@@ -7,5 +7,9 @@
href: logging.md
- name: Commands
href: commands.md
- name: FAQ
href: faq.md
- name: Voice
href: voice.md
- name: Events
href: events.md
- name: Code Samples
href: samples.md

+ 28
- 0
docs/guides/voice.md View File

@@ -0,0 +1,28 @@
# Voice

**Information on this page is subject to change!**

>[!WARNING]
>Audio in 1.0 is incomplete. Most of the below documentation is untested.

## Installation

To use Audio, you must first configure your `DiscordSocketClient` with Audio support.

In your @Discord.DiscordSocketConfig, set `AudioMode` to the appropriate @Discord.Audio.AudioMode for your bot. For most bots, you will only need to use `AudioMode.Outgoing`.

### Dependencies

Audio requires two native libraries, `libsodium` and `opus`. Both of these libraries must be placed in the runtime directory of your bot (for .NET 4.6, the directory where your exe is located; for .NET core, directory where your project.json is located)

For Windows Users, precompiled binaries are available for your convienence [here](https://discord.foxbot.me/binaries/)

For Linux Users, you will need to compile from source. [Sodium Source Code](https://download.libsodium.org/libsodium/releases/), [Opus Source Code](http://downloads.xiph.org/releases/opus/).

## Joining a Channel

Joining Voice Channels is relatively straight-forward, and is a requirement for sending or receiving audio. This will also allow us to create an @Discord.Audio.IAudioClient, which will be used later to send or receive audio.

[!code-csharp[Joining a Channel](samples/joining_audio.cs)]

The client will sustain a connection to this channel until it is kicked, disconnected from Discord, or told to disconnect.

+ 8
- 2
src/Discord.Net/API/DiscordRestApiClient.cs View File

@@ -357,6 +357,12 @@ namespace Discord.API

await SendAsync("DELETE", $"channels/{channelId}/pins/{messageId}", options: options).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<Message>> GetPinsAsync(ulong channelId, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));

return await SendAsync<IReadOnlyCollection<Message>>("GET", $"channels/{channelId}/pins", options: options).ConfigureAwait(false);
}

//Channel Recipients
public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null)
@@ -810,7 +816,7 @@ namespace Discord.API
{
return CreateMessageInternalAsync(0, channelId, args);
}
public async Task<Message> CreateMessageInternalAsync(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null)
private async Task<Message> CreateMessageInternalAsync(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotNull(args, nameof(args));
@@ -1010,7 +1016,7 @@ namespace Discord.API
public async Task ModifyMyNickAsync(ulong guildId, ModifyCurrentUserNickParams args, RequestOptions options = null)
{
Preconditions.NotNull(args, nameof(args));
Preconditions.NotEmpty(args.Nickname, nameof(args.Nickname));
Preconditions.NotNull(args.Nickname, nameof(args.Nickname));

await SendAsync("PATCH", $"guilds/{guildId}/members/@me/nick", args, options: options).ConfigureAwait(false);
}


+ 2
- 0
src/Discord.Net/Entities/Channels/IMessageChannel.cs View File

@@ -23,6 +23,8 @@ namespace Discord
Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Gets a collection of messages in this channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Gets a collection of pinned messages in this channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync();
/// <summary> Bulk deletes multiple messages. </summary>
Task DeleteMessagesAsync(IEnumerable<IMessage> messages);



+ 5
- 0
src/Discord.Net/Rest/Entities/Channels/DMChannel.cs View File

@@ -108,6 +108,11 @@ namespace Discord
{
await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync()
{
var models = await Discord.ApiClient.GetPinsAsync(Id);
return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray();
}

public async Task TriggerTypingAsync()
{


+ 5
- 0
src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs View File

@@ -133,6 +133,11 @@ namespace Discord
{
await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync()
{
var models = await Discord.ApiClient.GetPinsAsync(Id);
return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray();
}

public async Task TriggerTypingAsync()
{


+ 6
- 1
src/Discord.Net/Rest/Entities/Channels/TextChannel.cs View File

@@ -102,7 +102,12 @@ namespace Discord
{
await Discord.ApiClient.DeleteMessagesAsync(Guild.Id, Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync()
{
var models = await Discord.ApiClient.GetPinsAsync(Id);
return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray();
}

public async Task TriggerTypingAsync()
{
await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false);


+ 9
- 11
src/Discord.Net/Rpc/DiscordRpcClient.cs View File

@@ -115,6 +115,7 @@ namespace Discord.Rpc
/// <inheritdoc />
public async Task DisconnectAsync()
{
if (_connectTask?.TrySetCanceled() ?? false) return;
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
@@ -122,16 +123,6 @@ namespace Discord.Rpc
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectAsync(Exception ex, bool isReconnecting)
{
if (_connectTask?.TrySetException(ex) ?? false) return;
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync(ex, isReconnecting).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting)
{
if (!isReconnecting)
@@ -173,7 +164,14 @@ namespace Discord.Rpc
}
private async Task ReconnectInternalAsync(Exception ex, CancellationToken cancelToken)
{
await DisconnectAsync(null, true).ConfigureAwait(false);
if (ex == null)
{
if (_connectTask?.TrySetCanceled() ?? false) return;
}
else
{
if (_connectTask?.TrySetException(ex) ?? false) return;
}

try
{


+ 15
- 13
src/Discord.Net/WebSocket/DiscordSocketClient.cs View File

@@ -191,16 +191,6 @@ namespace Discord.WebSocket
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectAsync(Exception ex, bool isReconnecting)
{
if (_connectTask?.TrySetException(ex) ?? false) return;
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync(ex, isReconnecting).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting)
{
if (!isReconnecting)
@@ -270,7 +260,14 @@ namespace Discord.WebSocket
}
private async Task ReconnectInternalAsync(Exception ex, CancellationToken cancelToken)
{
await DisconnectAsync(null, true).ConfigureAwait(false);
if (ex == null)
{
if (_connectTask?.TrySetCanceled() ?? false) return;
}
else
{
if (_connectTask?.TrySetException(ex) ?? false) return;
}

try
{
@@ -580,7 +577,7 @@ namespace Discord.WebSocket
}
catch (Exception ex)
{
await DisconnectAsync(new Exception("Processing READY failed", ex), false);
_connectTask.TrySetException(new Exception("Processing READY failed", ex));
return;
}

@@ -1402,12 +1399,17 @@ namespace Discord.WebSocket
{
before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false);
after = guild.AddOrUpdateVoiceState(data, DataStore);
if (data.UserId == _currentUser.Id)
{
var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false);
}
}
else
{
before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false);
after = new VoiceState(null, data);
}

user = guild.GetUser(data.UserId);
}
else
@@ -1460,7 +1462,7 @@ namespace Discord.WebSocket
if (guild != null)
{
string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':'));
var _ = guild.ConnectAudio(_nextAudioId++, endpoint, data.Token).ConfigureAwait(false);
var _ = guild.FinishConnectAudio(_nextAudioId++, endpoint, data.Token).ConfigureAwait(false);
}
else
{


+ 3
- 5
src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

@@ -41,12 +41,10 @@ namespace Discord
var audioMode = Discord.AudioMode;
if (audioMode == AudioMode.Disabled)
throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set.");
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, Id,
(audioMode & AudioMode.Incoming) == 0,
return await Guild.ConnectAudioAsync(Id,
(audioMode & AudioMode.Incoming) == 0,
(audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false);
return null;
//TODO: Block and return
}

public SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel;


+ 104
- 19
src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -25,6 +25,7 @@ namespace Discord

private readonly SemaphoreSlim _audioLock;
private TaskCompletionSource<bool> _syncPromise, _downloaderPromise;
private TaskCompletionSource<AudioClient> _audioConnectPromise;
private ConcurrentHashSet<ulong> _channels;
private ConcurrentDictionary<ulong, SocketGuildUser> _members;
private ConcurrentDictionary<ulong, VoiceState> _voiceStates;
@@ -260,38 +261,99 @@ namespace Discord
return null;
}

public async Task ConnectAudio(int id, string url, string token)
public async Task<IAudioClient> ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute)
{
try
{
TaskCompletionSource<AudioClient> promise;

await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectAudioInternalAsync().ConfigureAwait(false);
promise = new TaskCompletionSource<AudioClient>();
_audioConnectPromise = promise;
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false);
}
finally
{
_audioLock.Release();
}

var timeoutTask = Task.Delay(15000);
if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask)
throw new TimeoutException();
return await promise.Task.ConfigureAwait(false);
}
catch (Exception)
{
await DisconnectAudioInternalAsync().ConfigureAwait(false);
throw;
}
}
public async Task DisconnectAudioAsync(AudioClient client = null)
{
AudioClient audioClient;
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectAudioInternalAsync(client).ConfigureAwait(false);
}
finally
{
_audioLock.Release();
}
}
private async Task DisconnectAudioInternalAsync(AudioClient client = null)
{
var oldClient = AudioClient;
if (oldClient != null)
{
if (client == null || oldClient == client)
{
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection
_audioConnectPromise = null;
}
if (oldClient == client)
{
AudioClient = null;
await oldClient.DisconnectAsync().ConfigureAwait(false);
}
}
}
public async Task FinishConnectAudio(int id, string url, string token)
{
var voiceState = GetVoiceState(CurrentUser.Id).Value;

await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
audioClient = AudioClient;
if (audioClient == null)
if (AudioClient == null)
{
audioClient = new AudioClient(this, id);
var audioClient = new AudioClient(this, id);
audioClient.Disconnected += async ex =>
{
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
if (ex != null)
if (AudioClient == audioClient) //Only reconnect if we're still assigned as this guild's audio client
{
//Reconnect if we still have channel info.
//TODO: Is this threadsafe? Could channel data be deleted before we access it?
var voiceState2 = GetVoiceState(CurrentUser.Id);
if (voiceState2.HasValue)
if (ex != null)
{
var voiceChannelId = voiceState2.Value.VoiceChannel?.Id;
if (voiceChannelId != null)
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted);
//Reconnect if we still have channel info.
//TODO: Is this threadsafe? Could channel data be deleted before we access it?
var voiceState2 = GetVoiceState(CurrentUser.Id);
if (voiceState2.HasValue)
{
var voiceChannelId = voiceState2.Value.VoiceChannel?.Id;
if (voiceChannelId != null)
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted);
}
}
else
{
try { AudioClient.Dispose(); } catch { }
AudioClient = null;
}
}
else
{
try { AudioClient.Dispose(); } catch { }
AudioClient = null;
}
}
finally
@@ -301,12 +363,35 @@ namespace Discord
};
AudioClient = audioClient;
}
await AudioClient.ConnectAsync(url, CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false);
await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await DisconnectAudioAsync();
}
catch (Exception e)
{
await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false);
await DisconnectAudioAsync();
}
finally
{
_audioLock.Release();
}
}
public async Task FinishJoinAudioChannel()
{
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
if (AudioClient != null)
await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false);
}
finally
{
_audioLock.Release();
}
await audioClient.ConnectAsync(url, CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false);
}

public SocketGuild Clone() => MemberwiseClone() as SocketGuild;


+ 1
- 1
src/Discord.Net/WebSocket/Extensions/ChannelExtensions.cs View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;

namespace Discord.WebSocket.Extensions
namespace Discord.WebSocket
{
public static class ChannelExtensions
{


+ 1
- 1
src/Discord.Net/WebSocket/Extensions/GuildExtensions.cs View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;

namespace Discord.WebSocket.Extensions
namespace Discord.WebSocket
{
// Todo: Docstrings
public static class GuildExtensions


Loading…
Cancel
Save