Author | SHA1 | Message | Date |
---|---|---|---|
|
8115aeabbf |
Merge pull request #399 from Grimitsu/master
Fix BuildInEngine.SendFile() error and suggestion to possible issue. |
8 years ago |
|
4e0cf92384 | Fix BuildInEngine.SendFile() error | 8 years ago |
|
d7507957ad |
Merge pull request #360 from Sentinent/master
Fixed connecting with user tokens and updated gateway OP codes |
8 years ago |
|
af478f224f | Fixed connecting with user tokens and updated gateway OP codes | 8 years ago |
|
34064ffe59 |
Merge pull request #316 from jvitkauskas/returnAfterThrow
remove return after throw (unreachable code) |
8 years ago |
|
2fbb4633a0 |
Merge pull request #317 from jvitkauskas/fixTypo
fix typo |
8 years ago |
|
1956d1df89 | fix typo | 8 years ago |
|
95b262a9fe | remove return after throw (unreachable code) | 8 years ago |
|
001dc525a3 | 0.9.6 | 8 years ago |
|
840ed338e5 |
Merge pull request #303 from Jeezymang/master
Added support for DND |
8 years ago |
|
0435ac933c | Amended to add invisible status | 8 years ago |
|
8fcb643cd0 | Added support for DND | 8 years ago |
|
04e4d9069a |
Update README.md
To appease @hydrabolt |
8 years ago |
|
42f2573358 |
Merge pull request #288 from moiph/moiph/sharding
Adds sharding support |
8 years ago |
|
8c1c16fe7b | Adds sharding support | 8 years ago |
|
cf3aa40c8a | Dealing with nuget's versioning restrictions | 8 years ago |
|
bbe61e118e | Redeployed Discord.Net.Audio | 8 years ago |
|
e220a30bf2 | Updated deps to 0.9.5 | 8 years ago |
|
d7b1c4577d | Fixed compile errors | 8 years ago |
|
22a7dab14c | 0.9.5 | 8 years ago |
|
23a53a7688 | Fixed README's Discord widget | 8 years ago |
|
aa0cb12ae7 |
Merge pull request #189 from Googie2149/master
Added bot authorization headers |
8 years ago |
|
2a19820c58 | Removed Bearer token type | 8 years ago |
|
541ca565fb |
Added bot authorization headers
Basically grabbed this from 1.0, thank you Rand for all the help |
8 years ago |
|
24c83f6674 | Removed use of obsolete events | 9 years ago |
|
6bfeaaddf0 |
Replaced expired invite
The invite to Discord API was expired. Replaced with the Vanity Invite |
9 years ago |
|
b07e96d517 |
Merge pull request #105 from AdenFlorian/event_summaries
Added XML Doc summaries to the Client Events |
9 years ago |
|
5195e6b230 | Added [Obsolete] attrib to 2 events | 9 years ago |
|
28d4041a5a | Added XML Doc summaries to the Client Events to help users of the library decide which events to use. | 9 years ago |
|
8db6eff8cb | Updated to .Net Core 1.0 RTM, 0.9.4 | 9 years ago |
|
b3b5d45dca | Removed build status badge | 9 years ago |
|
7808b1129c | Removed index.html | 9 years ago |
|
86406e2641 |
Merge pull request #101 from 420foxbot/patch-1
Added pretty badges |
9 years ago |
|
1de60a358e | Update docs link in README to legacy | 9 years ago |
|
d0e53bca6a | Updated README version and docs link | 9 years ago |
|
e7e5540d47 | 0.9.3.2 | 9 years ago |
|
6036de7736 |
Merge pull request #96 from khionu/master
BulkMessageDelete Exception |
9 years ago |
|
99637d0a21 | Separated the conditions | 9 years ago |
|
402d47959c | Included unsaved local changes | 9 years ago |
|
aeebe5d596 | Added OutOfBounds exception for DeleteMessages, for when Message Count is less than 2 or greater than 100 | 9 years ago |
|
d1ae0db741 | Dont send nickname in User.Edit unless one is specified | 9 years ago |
|
608d4a9cdd | Removed LegacyExtensions.Run() | 9 years ago |
|
1fb6a03e1d | Removed DiscordClient.Wait() | 9 years ago |
|
2b5bc17e98 | Added "around" direction | 9 years ago |
|
03c0f670f1 | Padded user discrminators | 9 years ago |
|
4e2b919de8 |
Merge pull request #74 from khionu/master
Add BulkMessageDelete Functionality |
9 years ago |
|
e3dea5b7c2 | Add BulkMessageDelete Functionality | 9 years ago |
|
f62948188d | 0.9.3.1 | 9 years ago |
|
eb14cfdb88 | Removed PreAuthenticate = false | 9 years ago |
|
a4b08bee8b | Fixed updating members without ManageNicknames | 9 years ago |
|
14180fb761 | Fixed null GameTypes | 9 years ago |
|
416162fd96 | 0.9.3 | 9 years ago |
|
80f9d6f2de | Added support for nickname parsing | 9 years ago |
|
bcbf1bc5c6 | Fixed setting other users' nicknames | 9 years ago |
|
12fad60fce | Fixed removing own nicknames | 9 years ago |
|
86b0b3b266 | Minor doc edit | 9 years ago |
|
b8cc2eb20f | Added commands triggering on nickname mentions | 9 years ago |
|
6d6137ad01 | Added Profile.NicknameMention | 9 years ago |
|
41629525e8 | Fixed game name not serializing | 9 years ago |
|
d0f6e70dbc | Several Game fixes | 9 years ago |
|
cc68ef27f5 | Fixed a couple permissions bugs | 9 years ago |
|
eb614d17b3 | Fixed a link | 9 years ago |
|
4d766aad7b | Removed Known issues | 9 years ago |
|
6a896da0b4 | Updated README | 9 years ago |
|
878ff65cf7 | Readded net45 support, 0.9.2 | 9 years ago |
|
4ea259bcef | Removed preserveCompilationContext | 9 years ago |
|
cd3d90ea80 | Added .Net Core RC2 Support | 9 years ago |
|
ec7297e99c | Dont exception on SetGame if no url is passed | 9 years ago |
|
94ad855290 |
Merge pull request #58 from khionu/master
Implimented the new GameObject for UserPresences |
9 years ago |
|
234185b603 | Confirmed working now | 9 years ago |
|
c3df2577ec | Implimented the new GameObject for UserPresences | 9 years ago |
|
a87027b7ab | Fixed several permission bugs | 9 years ago |
|
de2068140d | Cleaned up nickname editing | 9 years ago |
|
c6e919fb72 |
Merge pull request #57 from Lirusaito/master
Add support for changing Nicknames! |
9 years ago |
|
d1f3dfe26a | Shove in support for changing own nick (through `User.Edit(nickname:))` | 9 years ago |
|
641401da4d |
Add support for changing Nicknames!
User.Edit(nickname: "foo"); |
9 years ago |
|
9c77a80cf5 | Typo | 9 years ago |
|
75a5f4a739 | Added Administrator, ChangeNick and ManageNick permissions | 9 years ago |
|
7606dd650f | Use cancelTokens in LargeServerDownloader | 9 years ago |
|
f04789619a | Fixed DiscordClient.Disconnect stalling | 9 years ago |
|
64d4b0af32 | Support nickname mentions in FindUsers | 9 years ago |
|
69b53b2ce5 |
Merge pull request #55 from khionu/master
Added SelfBot Functionality by Config |
9 years ago |
|
f74a393581 |
Merge pull request #56 from Lirusaito/master
Support Role Mentions |
9 years ago |
|
6337266375 | Restore ability to `.Mention` the `EveryoneRole` | 9 years ago |
|
549b32de6d |
Support Role Mentions
`Message.MentionedRoles` is now populated properly with roles. `Role.IsMentionable` has been added to determine whether `Role.Mention` now mentions roles that are `IsMentionable` as expected (otherwise returns empty string) `Role.Edit` now takes parameter `bool? isMentionable` `Server.CreateRole` now takes parameter `bool isMentionable` `IsMentionable` added to `UpdateRoleRequest` |
9 years ago |
|
afe1b318e8 | Remove <see/> from Summary | 9 years ago |
|
d30f462afc | Added SelfBot Setting | 9 years ago |
|
5042ae22dd |
Merge pull request #54 from 420foxbot/patch-user-nickname
Add Nicknames to User Model; Bump README to 0.9.1 |
9 years ago |
|
440e193fdf | Add Nicknames to User Model; Bump README to 0.9.1 | 9 years ago |
|
58e9716158 | Actually bumped the version to 0.9.1 | 9 years ago |
|
033ea96204 |
Merge pull request #52 from 420foxbot/master-user-bot
Add `User.IsBot` field, bump version number to 0.9.1 |
9 years ago |
|
789419ac86 | don't check for null and default to false | 9 years ago |
|
c012261cd1 |
Add `User.IsBot` field, bump version number to 0.9rc4
A few users requested an IsBot field for marking Bot Accounts; Bumping Version number because it has been 0.9-rc3 forever and that's caused a bit of confusion in the channel with what version users are on. |
9 years ago |
|
4f632df446 | Don't cache DefaultChannel or EveryoneRole | 9 years ago |
|
46e5b44483 | Removed EnableMultiserver | 9 years ago |
|
638fe71f6e |
Merge pull request #49 from khionu/master
Removed hack |
9 years ago |
|
58025d303f | Removed hack; code now assumes client is logged in as Bot Account, without 1 voice channel limit | 9 years ago |
|
4d8e5b878f | Made large server downloader more stable | 9 years ago |
|
87c9d2243b |
Merge pull request #44 from moiph/master
check for server==null in User.ServerPermissions getter |
9 years ago |
|
0a068bb63c | PR cleanup, adjusting delay times | 9 years ago |
|
8a8b01cc5b |
Merge pull request #47 from Kwoth/master
Voice crashfix, Properly getting data for large servers. |
9 years ago |
|
e72d681b96 | Removed unused variable | 9 years ago |
|
f7ba519989 | Changed loglevel to Verbose for large server data message | 9 years ago |
|
765e986018 | Now getting all of the large server data properly | 9 years ago |
|
659e63f2b4 | Voice Socket error fix | 9 years ago |
|
8f0bc1933a | Add a timeout to guild member chunking, fix voice connections, delay READY. | 9 years ago |
|
0361f2e2d6 | check for server==null in User.ServerPermissions getter | 9 years ago |
|
a44d7de473 | This is what I get for pushing to master | 9 years ago |
|
ce23943b45 | v4 bugfix | 9 years ago |
|
35ec23d33c | Pass v4 info to the right endpoint | 9 years ago |
|
24e71f7c1a | Request gateway v4, support reconnects | 9 years ago |
|
a5beded156 | Fixed READY crash on v4 gateway | 9 years ago |
|
a357a06d33 | Added delay to member request batching | 9 years ago |
|
b7edf48a19 | Actually batch, not just offset... | 9 years ago |
|
db295bac3a | Minor changes | 9 years ago |
|
4e3f726073 | Added member request batching | 9 years ago |
|
1a18d920db |
Merge pull request #42 from 420foxbot/unit-testing
Preliminary Unit-Testing |
9 years ago |
|
20b4288f57 | Update README.md for Unit Test Badge; Docs | 9 years ago |
|
8522c8b5b0 |
Add Unit Tests for User Operations
- Test User Mentions - Test User Edit [UserUpdatedEventArgs] - Test CurrentUser.Edit [UserUpdatedEventArgs] - Test SetStatus - Test SetGame |
9 years ago |
|
bf41b30a9a |
Added more Unit Tests for Messages, Channels; Removed Settings
- Test for Server.GetChannel - Test for SendTyping [ChannelUserEventArgs] - Test for EditChannel [ChannelUpdatedEventArgs] - Test for Channel Mentions - Test for DownloadMessages with/without cache - Test for SendTTSMessage |
9 years ago |
|
c887adfa4b |
Unit tests for Permissions
- Test AddGet Permissions - Test AddRemove Permissions - Test Permissions triggering ChannelUpdated - Test HttpException triggered when affecting permissions in an invalid channel. |
9 years ago |
|
3f484f4c19 |
deprecated config.json
Environment Vars will be used for storing credentials, to maintain compatibility on the CI. |
9 years ago |
|
654396e665 |
Added basic Permissions testing
Test adding Permissions Rules and retrieving Permissions Rules |
9 years ago |
|
9b403e872a |
Removed exception testing for messages
MessageQueue silently handles all exceptions, cannot test for them. |
9 years ago |
|
75ee7b0913 |
Message Unit Tests
Test Events for Send/Update/Delete Message Test Exceptions for: - Send to a Deleted Channel - Send to a Channel with No Perms - Update a Deleted Message |
9 years ago |
|
9e78ca4941 |
Begin rewriting Unit Tests
Fixed up existing code for Unit Tests, Added EventArgs-less AssertEvent, Added Tests for READY and JoinedServer Temporarily moved initialization to it's own Test, may leave there? |
9 years ago |
|
25f148d5a8 | Several bugfixes | 9 years ago |
|
e55f1a6415 | Merge branch 'master' of https://github.com/RogueException/Discord.Net.git | 9 years ago |
|
a072a44481 |
Merge pull request #35 from SSStormy/patch-1
Fix Server.RoleCount returning channel amount. |
9 years ago |
|
c33a1f2cd3 | Fixed Server.RoleCount returning channel amount. | 9 years ago |
@@ -1,10 +1,9 @@ | |||
# Discord.Net v0.9.0-rc3 | |||
An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). | |||
# Discord.Net v0.9.6 | |||
[](https://www.nuget.org/packages/Discord.Net) [](https://discord.gg/0SBTUU1wZTYLhAAW) | |||
Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). | |||
An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). | |||
##### Warning: documentation is currently outdated. | |||
It's current being rewritten. Until that's done, feel free to use my [DiscordBot](https://github.com/RogueException/DiscordBot) repo for reference. | |||
Check out the [documentation](http://rtd.discord.foxbot.me/en/legacy/) or join the [Discord API Chat](https://discord.gg/discord-api). | |||
### Installation | |||
You can download Discord.Net and its extensions from NuGet: | |||
@@ -16,9 +15,6 @@ You can download Discord.Net and its extensions from NuGet: | |||
### Compiling | |||
In order to compile Discord.Net, you require at least the following: | |||
- [Visual Studio 2015](https://www.visualstudio.com/downloads/download-visual-studio-vs) | |||
- Visual Studio 2015 Update 1 (available through Visual Studio) | |||
- [ASP.Net 5 RC1](https://get.asp.net) | |||
- NuGet 3.3 (available through Visual Studio) | |||
### Known Issues | |||
- .Net Core support is incomplete on non-Windows systems | |||
- [Visual Studio 2015 Update 3](https://www.microsoft.com/net/core#windows) | |||
- [Visual Studio .Net Core Plugin](https://www.microsoft.com/net/core#windows) | |||
- NuGet 3.3+ (available through Visual Studio) |
@@ -1,6 +1,3 @@ | |||
{ | |||
"projects": [ "src" ], | |||
"sdk": { | |||
"version": "1.0.0-rc1-update1" | |||
} | |||
"projects": [ "src" ] | |||
} |
@@ -13,6 +13,6 @@ using System.Runtime.InteropServices; | |||
[assembly: ComVisible(false)] | |||
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | |||
[assembly: AssemblyVersion("0.9.0.0")] | |||
[assembly: AssemblyFileVersion("0.9.0.0")] | |||
[assembly: AssemblyVersion("0.9.6.0")] | |||
[assembly: AssemblyFileVersion("0.9.6.0")] | |||
@@ -1,5 +1,4 @@ | |||
using Discord.API.Client.GatewaySocket; | |||
using Discord.API.Client.Rest; | |||
using Discord.Logging; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
@@ -13,7 +12,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal class AudioClient : IAudioClient | |||
internal class AudioClient : IAudioClient | |||
{ | |||
private class OutStream : Stream | |||
{ | |||
@@ -50,7 +49,7 @@ namespace Discord.Audio | |||
private ConnectionState _gatewayState; | |||
internal Logger Logger { get; } | |||
public int Id { get; } | |||
public AudioService Service { get; } | |||
public AudioServiceConfig Config { get; } | |||
@@ -59,7 +58,7 @@ namespace Discord.Audio | |||
public VoiceSocket VoiceSocket { get; } | |||
public JsonSerializer Serializer { get; } | |||
public Stream OutputStream { get; } | |||
public CancellationToken CancelToken { get; private set; } | |||
public string SessionId => GatewaySocket.SessionId; | |||
@@ -68,7 +67,7 @@ namespace Discord.Audio | |||
public Channel Channel => VoiceSocket.Channel; | |||
public AudioClient(DiscordClient client, Server server, int id) | |||
{ | |||
{ | |||
Id = id; | |||
Service = client.GetService<AudioService>(); | |||
Config = Service.Config; | |||
@@ -84,40 +83,8 @@ namespace Discord.Audio | |||
CancelToken = new CancellationToken(true); | |||
//Networking | |||
if (Config.EnableMultiserver) | |||
{ | |||
//TODO: We can remove this hack when official API launches | |||
var baseConfig = client.Config; | |||
var builder = new DiscordConfigBuilder | |||
{ | |||
AppName = baseConfig.AppName, | |||
AppUrl = baseConfig.AppUrl, | |||
AppVersion = baseConfig.AppVersion, | |||
CacheToken = baseConfig.CacheDir != null, | |||
ConnectionTimeout = baseConfig.ConnectionTimeout, | |||
EnablePreUpdateEvents = false, | |||
FailedReconnectDelay = baseConfig.FailedReconnectDelay, | |||
LargeThreshold = 1, | |||
LogLevel = baseConfig.LogLevel, | |||
MessageCacheSize = 0, | |||
ReconnectDelay = baseConfig.ReconnectDelay, | |||
UsePermissionsCache = false | |||
}; | |||
_config = builder.Build(); | |||
ClientAPI = new JsonRestClient(_config, DiscordConfig.ClientAPIUrl, client.Log.CreateLogger($"ClientAPI #{id}")); | |||
GatewaySocket = new GatewaySocket(_config, client.Serializer, client.Log.CreateLogger($"Gateway #{id}")); | |||
GatewaySocket.Connected += (s, e) => | |||
{ | |||
if (_gatewayState == ConnectionState.Connecting) | |||
EndGatewayConnect(); | |||
}; | |||
} | |||
else | |||
{ | |||
_config = client.Config; | |||
GatewaySocket = client.GatewaySocket; | |||
} | |||
_config = client.Config; | |||
GatewaySocket = client.GatewaySocket; | |||
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); | |||
VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}")); | |||
VoiceSocket.Server = server; | |||
@@ -126,14 +93,9 @@ namespace Discord.Audio | |||
public async Task Connect() | |||
{ | |||
if (Config.EnableMultiserver) | |||
await BeginGatewayConnect().ConfigureAwait(false); | |||
else | |||
{ | |||
var cancelSource = new CancellationTokenSource(); | |||
CancelToken = cancelSource.Token; | |||
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); | |||
} | |||
var cancelSource = new CancellationTokenSource(); | |||
CancelToken = cancelSource.Token; | |||
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); | |||
} | |||
private async Task BeginGatewayConnect() | |||
{ | |||
@@ -154,7 +116,7 @@ namespace Discord.Audio | |||
var cancelSource = new CancellationTokenSource(); | |||
CancelToken = cancelSource.Token; | |||
ClientAPI.CancelToken = CancelToken; | |||
await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); | |||
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); | |||
@@ -178,40 +140,23 @@ namespace Discord.Audio | |||
{ | |||
_gatewayState = ConnectionState.Connected; | |||
} | |||
public async Task Disconnect() | |||
{ | |||
await _taskManager.Stop(true).ConfigureAwait(false); | |||
if (Config.EnableMultiserver) | |||
ClientAPI.Token = null; | |||
} | |||
private async Task Cleanup() | |||
{ | |||
var oldState = _gatewayState; | |||
_gatewayState = ConnectionState.Disconnecting; | |||
if (Config.EnableMultiserver) | |||
{ | |||
if (oldState == ConnectionState.Connected) | |||
{ | |||
try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } | |||
catch (OperationCanceledException) { } | |||
} | |||
await GatewaySocket.Disconnect().ConfigureAwait(false); | |||
ClientAPI.Token = null; | |||
} | |||
var server = VoiceSocket.Server; | |||
VoiceSocket.Server = null; | |||
VoiceSocket.Channel = null; | |||
if (Config.EnableMultiserver) | |||
await Service.RemoveClient(server, this).ConfigureAwait(false); | |||
await Service.RemoveClient(server, this).ConfigureAwait(false); | |||
SendVoiceUpdate(server.Id, null); | |||
await VoiceSocket.Disconnect().ConfigureAwait(false); | |||
if (Config.EnableMultiserver) | |||
await GatewaySocket.Disconnect().ConfigureAwait(false); | |||
_gatewayState = (int)ConnectionState.Disconnected; | |||
} | |||
@@ -224,7 +169,7 @@ namespace Discord.Audio | |||
if (channel == VoiceSocket.Channel) return; | |||
var server = channel.Server; | |||
if (server != VoiceSocket.Server) | |||
throw new ArgumentException("This is channel is not part of the current server.", nameof(channel)); | |||
throw new ArgumentException("This channel is not part of the current server.", nameof(channel)); | |||
if (VoiceSocket.Server == null) | |||
throw new InvalidOperationException("This client has been closed."); | |||
@@ -284,26 +229,26 @@ namespace Discord.Audio | |||
} | |||
public void Send(byte[] data, int offset, int count) | |||
{ | |||
{ | |||
if (data == null) throw new ArgumentException(nameof(data)); | |||
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); | |||
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(count)); | |||
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
if (count == 0) return; | |||
VoiceSocket.SendPCMFrames(data, offset, count); | |||
} | |||
VoiceSocket.SendPCMFrames(data, offset, count); | |||
} | |||
public void Clear() | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.ClearPCMFrames(); | |||
} | |||
public void Wait() | |||
public void Wait() | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.WaitForQueue(); | |||
} | |||
} | |||
public void SendVoiceUpdate(ulong? serverId, ulong? channelId) | |||
{ | |||
@@ -311,5 +256,5 @@ namespace Discord.Audio | |||
(Service.Config.Mode | AudioMode.Outgoing) == 0, | |||
(Service.Config.Mode | AudioMode.Incoming) == 0); | |||
} | |||
} | |||
} | |||
} |
@@ -1,22 +1,19 @@ | |||
using Nito.AsyncEx; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public class AudioService : IService | |||
public class AudioService : IService | |||
{ | |||
private readonly AsyncLock _asyncLock; | |||
private AudioClient _defaultClient; //Only used for single server | |||
private VirtualClient _currentClient; //Only used for single server | |||
private ConcurrentDictionary<ulong, AudioClient> _voiceClients; | |||
private ConcurrentDictionary<User, bool> _talkingUsers; | |||
private int _nextClientId; | |||
private ConcurrentDictionary<User, bool> _talkingUsers; | |||
private int _nextClientId; | |||
public DiscordClient Client { get; private set; } | |||
public AudioServiceConfig Config { get; } | |||
public AudioServiceConfig Config { get; } | |||
public event EventHandler Connected = delegate { }; | |||
public event EventHandler<VoiceDisconnectedEventArgs> Disconnected = delegate { }; | |||
@@ -24,9 +21,9 @@ namespace Discord.Audio | |||
private void OnConnected() | |||
=> Connected(this, EventArgs.Empty); | |||
private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex) | |||
private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex) | |||
=> Disconnected(this, new VoiceDisconnectedEventArgs(serverId, wasUnexpected, ex)); | |||
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking) | |||
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking) | |||
=> UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking)); | |||
public AudioService() | |||
@@ -38,72 +35,41 @@ namespace Discord.Audio | |||
{ | |||
} | |||
public AudioService(AudioServiceConfig config) | |||
{ | |||
{ | |||
Config = config; | |||
_asyncLock = new AsyncLock(); | |||
} | |||
void IService.Install(DiscordClient client) | |||
{ | |||
Client = client; | |||
if (Config.EnableMultiserver) | |||
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>(); | |||
else | |||
{ | |||
var logger = Client.Log.CreateLogger("Voice"); | |||
_defaultClient = new AudioClient(Client, null, 0); | |||
} | |||
_talkingUsers = new ConcurrentDictionary<User, bool>(); | |||
client.GatewaySocket.Disconnected += async (s, e) => | |||
{ | |||
if (Config.EnableMultiserver) | |||
{ | |||
var tasks = _voiceClients | |||
.Select(x => | |||
{ | |||
var val = x.Value; | |||
if (val != null) | |||
return x.Value.Disconnect(); | |||
else | |||
return TaskHelper.CompletedTask; | |||
}) | |||
.ToArray(); | |||
await Task.WhenAll(tasks).ConfigureAwait(false); | |||
_voiceClients.Clear(); | |||
} | |||
foreach (var member in _talkingUsers) | |||
{ | |||
bool ignored; | |||
if (_talkingUsers.TryRemove(member.Key, out ignored)) | |||
OnUserIsSpeakingUpdated(member.Key, false); | |||
} | |||
}; | |||
} | |||
public IAudioClient GetClient(Server server) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
if (Config.EnableMultiserver) | |||
void IService.Install(DiscordClient client) | |||
{ | |||
Client = client; | |||
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>(); | |||
_talkingUsers = new ConcurrentDictionary<User, bool>(); | |||
client.GatewaySocket.Disconnected += (s, e) => | |||
{ | |||
AudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
else | |||
return null; | |||
} | |||
foreach (var member in _talkingUsers) | |||
{ | |||
bool ignored; | |||
if (_talkingUsers.TryRemove(member.Key, out ignored)) | |||
OnUserIsSpeakingUpdated(member.Key, false); | |||
} | |||
}; | |||
} | |||
public IAudioClient GetClient(Server server) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
AudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
else | |||
{ | |||
if (server == _currentClient.Server) | |||
return _currentClient; | |||
else | |||
return null; | |||
} | |||
} | |||
//Called from AudioClient.Disconnect | |||
return null; | |||
} | |||
//Called from AudioClient.Cleanup | |||
internal async Task RemoveClient(Server server, AudioClient client) | |||
{ | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
@@ -113,81 +79,40 @@ namespace Discord.Audio | |||
} | |||
} | |||
public async Task<IAudioClient> Join(Channel channel) | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
public async Task<IAudioClient> Join(Channel channel) | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
var server = channel.Server; | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
if (Config.EnableMultiserver) | |||
{ | |||
AudioClient client; | |||
if (!_voiceClients.TryGetValue(server.Id, out client)) | |||
{ | |||
client = new AudioClient(Client, server, unchecked(++_nextClientId)); | |||
_voiceClients[server.Id] = client; | |||
await client.Connect().ConfigureAwait(false); | |||
/*voiceClient.VoiceSocket.FrameReceived += (s, e) => | |||
{ | |||
OnFrameReceieved(e); | |||
}; | |||
voiceClient.VoiceSocket.UserIsSpeaking += (s, e) => | |||
{ | |||
var user = server.GetUser(e.UserId); | |||
OnUserIsSpeakingUpdated(user, e.IsSpeaking); | |||
};*/ | |||
} | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
} | |||
else | |||
AudioClient client; | |||
if (!_voiceClients.TryGetValue(server.Id, out client)) | |||
{ | |||
if (_defaultClient.Server != server) | |||
{ | |||
await _defaultClient.Disconnect().ConfigureAwait(false); | |||
_defaultClient.VoiceSocket.Server = server; | |||
await _defaultClient.Connect().ConfigureAwait(false); | |||
} | |||
var client = new VirtualClient(_defaultClient, server); | |||
_currentClient = client; | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
client = new AudioClient(Client, server, unchecked(++_nextClientId)); | |||
_voiceClients[server.Id] = client; | |||
await client.Connect().ConfigureAwait(false); | |||
} | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
} | |||
} | |||
} | |||
public Task Leave(Server server) => Leave(server, null); | |||
public Task Leave(Server server) => Leave(server, null); | |||
public Task Leave(Channel channel) => Leave(channel.Server, channel); | |||
private async Task Leave(Server server, Channel channel) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
if (Config.EnableMultiserver) | |||
AudioClient client; | |||
//Potential race condition if changing channels during this call, but that's acceptable | |||
if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel)) | |||
{ | |||
AudioClient client; | |||
//Potential race condition if changing channels during this call, but that's acceptable | |||
if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel)) | |||
{ | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
await client.Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
else | |||
{ | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
var client = GetClient(server) as VirtualClient; | |||
if (client != null && client.Channel == channel) | |||
await _defaultClient.Disconnect().ConfigureAwait(false); | |||
} | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
await client.Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
} |
@@ -7,11 +7,6 @@ | |||
/// <summary> Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. </summary> | |||
public bool EnableEncryption { get; set; } = true; | |||
/// <summary> | |||
/// Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). | |||
/// This option uses a lot of CPU power and network bandwidth, as a new gateway connection needs to be spun up per server. Use sparingly. | |||
/// </summary> | |||
public bool EnableMultiserver { get; set; } = false; | |||
/// <summary> Gets or sets the buffer length (in milliseconds) for outgoing voice packets. </summary> | |||
public int BufferLength { get; set; } = 1000; | |||
@@ -30,7 +25,6 @@ | |||
public AudioMode Mode { get; } | |||
public bool EnableEncryption { get; } | |||
public bool EnableMultiserver { get; } | |||
public int BufferLength { get; } | |||
public int? Bitrate { get; } | |||
@@ -41,7 +35,6 @@ | |||
Mode = builder.Mode; | |||
EnableEncryption = builder.EnableEncryption; | |||
EnableMultiserver = builder.EnableMultiserver; | |||
BufferLength = builder.BufferLength; | |||
Bitrate = builder.Bitrate; | |||
@@ -4,12 +4,12 @@ | |||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> | |||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<PropertyGroup Label="Globals"> | |||
<ProjectGuid>dff7afe3-ca77-4109-bade-b4b49a4f6648</ProjectGuid> | |||
<RootNamespace>Discord.Audio</RootNamespace> | |||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> | |||
</PropertyGroup> | |||
<PropertyGroup> | |||
<SchemaVersion>2.0</SchemaVersion> | |||
@@ -17,5 +17,5 @@ | |||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> | |||
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
</Project> |
@@ -120,7 +120,7 @@ namespace Discord.Net.WebSockets | |||
SendIdentify(_userId.Value, _sessionId); | |||
#if !DOTNET5_4 | |||
#if !NETSTANDARD1_3 | |||
tasks.Add(WatcherAsync()); | |||
#endif | |||
tasks.AddRange(_engine.GetTasks(CancelToken)); | |||
@@ -178,7 +178,7 @@ namespace Discord.Net.WebSockets | |||
await Task.Delay(1).ConfigureAwait(false); | |||
if (_udp.Available > 0) | |||
{ | |||
#if !DOTNET5_4 | |||
#if !NETSTANDARD1_3 | |||
packet = _udp.Receive(ref endpoint); | |||
#else | |||
//TODO: Is this really the only way to end a Receive call in DOTNET5_4? | |||
@@ -346,7 +346,7 @@ namespace Discord.Net.WebSockets | |||
{ | |||
try | |||
{ | |||
_udp.Send(voicePacket, rtpPacketLength); | |||
await _udp.SendAsync(voicePacket, rtpPacketLength, _endpoint).ConfigureAwait(false); | |||
} | |||
catch (SocketException ex) | |||
{ | |||
@@ -371,7 +371,7 @@ namespace Discord.Net.WebSockets | |||
break; | |||
} | |||
} | |||
await _udp.SendAsync(pingPacket, pingPacket.Length).ConfigureAwait(false); | |||
await _udp.SendAsync(pingPacket, pingPacket.Length, _endpoint).ConfigureAwait(false); | |||
nextPingTicks = currentTicks + 5 * ticksPerSeconds; | |||
} | |||
} | |||
@@ -391,7 +391,7 @@ namespace Discord.Net.WebSockets | |||
catch (OperationCanceledException) { } | |||
catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||
} | |||
#if !DOTNET5_4 | |||
#if !NETSTANDARD1_3 | |||
//Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken | |||
private async Task WatcherAsync() | |||
{ | |||
@@ -407,7 +407,7 @@ namespace Discord.Net.WebSockets | |||
WebSocketMessage msg; | |||
using (var reader = new JsonTextReader(new StringReader(json))) | |||
msg = _serializer.Deserialize(reader, typeof(WebSocketMessage)) as WebSocketMessage; | |||
var opCode = (OpCodes)msg.Operation; | |||
switch (opCode) | |||
{ | |||
@@ -418,7 +418,8 @@ namespace Discord.Net.WebSockets | |||
var payload = (msg.Payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
_heartbeatInterval = payload.HeartbeatInterval; | |||
_ssrc = payload.SSRC; | |||
var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(); | |||
string hostname = Host.Replace("wss://", ""); | |||
var address = (await Dns.GetHostAddressesAsync(hostname).ConfigureAwait(false)).FirstOrDefault(); | |||
_endpoint = new IPEndPoint(address, payload.Port); | |||
if (_audioConfig.EnableEncryption) | |||
@@ -436,7 +437,6 @@ namespace Discord.Net.WebSockets | |||
_encryptionMode = UnencryptedMode; | |||
_isEncrypted = false; | |||
} | |||
_udp.Connect(_endpoint); | |||
_sequence = 0;// (ushort)_rand.Next(0, ushort.MaxValue); | |||
//No thread issue here because SendAsync doesn't start until _isReady is true | |||
@@ -445,7 +445,7 @@ namespace Discord.Net.WebSockets | |||
packet[1] = (byte)(_ssrc >> 16); | |||
packet[2] = (byte)(_ssrc >> 8); | |||
packet[3] = (byte)(_ssrc >> 0); | |||
await _udp.SendAsync(packet, 70).ConfigureAwait(false); | |||
await _udp.SendAsync(packet, 70, _endpoint).ConfigureAwait(false); | |||
} | |||
} | |||
break; | |||
@@ -1,27 +1,40 @@ | |||
{ | |||
"version": "0.9.0-rc3", | |||
"version": "0.9.6", | |||
"description": "A Discord.Net extension adding voice support.", | |||
"authors": [ "RogueException" ], | |||
"tags": [ "discord", "discordapp" ], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
"packOptions": { | |||
"tags": [ "discord", "discordapp" ], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
}, | |||
"contentFiles": [ "libsodium.dll", "opus.dll" ] | |||
}, | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"contentFiles": [ "libsodium.dll", "opus.dll" ], | |||
"compilationOptions": { | |||
"buildOptions": { | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"allowUnsafe": true, | |||
"warningsAsErrors": true | |||
}, | |||
"dependencies": { | |||
"Discord.Net": "0.9.0-rc3-3" | |||
"Discord.Net": "0.9.6" | |||
}, | |||
"frameworks": { | |||
"net45": { }, | |||
"dotnet5.4": { } | |||
"netstandard1.3": { | |||
"dependencies": { | |||
"NETStandard.Library": "1.6.0" | |||
}, | |||
"imports": [ | |||
"dotnet5.4", | |||
"dnxcore50", | |||
"portable-net45+win8" | |||
] | |||
}, | |||
"net45": { } | |||
} | |||
} |
@@ -13,6 +13,6 @@ using System.Runtime.InteropServices; | |||
[assembly: ComVisible(false)] | |||
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | |||
[assembly: AssemblyVersion("0.9.0.0")] | |||
[assembly: AssemblyFileVersion("0.9.0.0")] | |||
[assembly: AssemblyVersion("0.9.6.0")] | |||
[assembly: AssemblyFileVersion("0.9.6.0")] | |||
@@ -80,7 +80,13 @@ namespace Discord.Commands | |||
client.MessageReceived += async (s, e) => | |||
{ | |||
if (_allCommands.Count == 0) return; | |||
if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return; | |||
if (Config.IsSelfBot) | |||
{ | |||
if (e.Message.User == null || e.Message.User.Id != Client.CurrentUser.Id) return; // Will only listen to Self | |||
} | |||
else | |||
if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return; // Normal expected behavior for bots | |||
string msg = e.Message.RawText; | |||
if (msg.Length == 0) return; | |||
@@ -106,6 +112,19 @@ namespace Discord.Commands | |||
if (msg.StartsWith(mention) && msg.Length > mention.Length) | |||
cmdMsg = msg.Substring(mention.Length + 1); | |||
} | |||
string mention2 = client.CurrentUser.NicknameMention; | |||
if (mention2 != null) | |||
{ | |||
if (msg.StartsWith(mention2) && msg.Length > mention2.Length) | |||
cmdMsg = msg.Substring(mention2.Length + 1); | |||
else | |||
{ | |||
mention2 = $"@{client.CurrentUser.Name}"; | |||
if (msg.StartsWith(mention2) && msg.Length > mention2.Length) | |||
cmdMsg = msg.Substring(mention2.Length + 1); | |||
} | |||
} | |||
} | |||
//Check using custom activator | |||
@@ -14,6 +14,11 @@ namespace Discord.Commands | |||
/// and a negative value if the message should be ignored. | |||
/// </summary> | |||
public Func<Message, int> CustomPrefixHandler { get; set; } = null; | |||
/// <summary> | |||
/// Changing this to true makes the bot ignore all messages, except when the messages are from its own account. | |||
/// This is desired behavior for "Self Bots" only, so unless this bot is being run under a normal user's account, leave it alone!! | |||
/// </summary> | |||
public bool IsSelfBot { get; set; } = false; | |||
/// <summary> Gets or sets whether a help function should be automatically generated. </summary> | |||
public HelpMode HelpMode { get; set; } = HelpMode.Disabled; | |||
@@ -28,9 +33,10 @@ namespace Discord.Commands | |||
} | |||
public class CommandServiceConfig | |||
{ | |||
public char? PrefixChar { get; } | |||
public char? PrefixChar { get; } | |||
public bool AllowMentionPrefix { get; } | |||
public Func<Message, int> CustomPrefixHandler { get; } | |||
public bool IsSelfBot { get; } | |||
/// <summary> Gets or sets whether a help function should be automatically generated. </summary> | |||
public HelpMode HelpMode { get; set; } = HelpMode.Disabled; | |||
@@ -41,6 +47,7 @@ namespace Discord.Commands | |||
AllowMentionPrefix = builder.AllowMentionPrefix; | |||
CustomPrefixHandler = builder.CustomPrefixHandler; | |||
HelpMode = builder.HelpMode; | |||
IsSelfBot = builder.IsSelfBot; | |||
} | |||
} | |||
} |
@@ -4,12 +4,12 @@ | |||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> | |||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<PropertyGroup Label="Globals"> | |||
<ProjectGuid>19793545-ef89-48f4-8100-3ebaad0a9141</ProjectGuid> | |||
<RootNamespace>Discord.Commands</RootNamespace> | |||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> | |||
</PropertyGroup> | |||
<PropertyGroup> | |||
<SchemaVersion>2.0</SchemaVersion> | |||
@@ -17,5 +17,5 @@ | |||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> | |||
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
</Project> |
@@ -1,25 +1,38 @@ | |||
{ | |||
"version": "0.9.0-rc3-1", | |||
"version": "0.9.6", | |||
"description": "A Discord.Net extension adding basic command support.", | |||
"authors": [ "RogueException" ], | |||
"tags": [ "discord", "discordapp" ], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
"packOptions": { | |||
"tags": [ "discord", "discordapp" ], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
} | |||
}, | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"compilationOptions": { | |||
"buildOptions": { | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"warningsAsErrors": true | |||
}, | |||
"dependencies": { | |||
"Discord.Net": "0.9.0-rc3-3" | |||
"Discord.Net": "0.9.6" | |||
}, | |||
"frameworks": { | |||
"net45": { }, | |||
"dotnet5.4": { } | |||
"netstandard1.3": { | |||
"dependencies": { | |||
"NETStandard.Library": "1.6.0" | |||
}, | |||
"imports": [ | |||
"dotnet5.4", | |||
"dnxcore50", | |||
"portable-net45+win8" | |||
] | |||
}, | |||
"net45": {} | |||
} | |||
} |
@@ -13,6 +13,6 @@ using System.Runtime.InteropServices; | |||
[assembly: ComVisible(false)] | |||
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | |||
[assembly: AssemblyVersion("0.9.0.0")] | |||
[assembly: AssemblyFileVersion("0.9.0.0")] | |||
[assembly: AssemblyVersion("0.9.6.0")] | |||
[assembly: AssemblyFileVersion("0.9.6.0")] | |||
@@ -4,12 +4,12 @@ | |||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> | |||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<PropertyGroup Label="Globals"> | |||
<ProjectGuid>01584e8a-78da-486f-9ef9-a894e435841b</ProjectGuid> | |||
<RootNamespace>Discord.Modules</RootNamespace> | |||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> | |||
</PropertyGroup> | |||
<PropertyGroup> | |||
<SchemaVersion>2.0</SchemaVersion> | |||
@@ -17,5 +17,5 @@ | |||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> | |||
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
</Project> |
@@ -43,15 +43,15 @@ namespace Discord.Modules | |||
public event EventHandler<UserEventArgs> UserJoined = delegate { }; | |||
public event EventHandler<UserEventArgs> UserLeft = delegate { }; | |||
public event EventHandler<UserUpdatedEventArgs> UserUpdated = delegate { }; | |||
//public event EventHandler<UserEventArgs> UserPresenceUpdated = delegate { }; | |||
//public event EventHandler<UserEventArgs> UserVoiceStateUpdated = delegate { }; | |||
public event EventHandler<UserEventArgs> UserUnbanned = delegate { }; | |||
public event EventHandler<ChannelUserEventArgs> UserIsTyping = delegate { }; | |||
public event EventHandler<MessageEventArgs> MessageReceived = delegate { }; | |||
[Obsolete] | |||
public event EventHandler<MessageEventArgs> MessageSent = delegate { }; | |||
public event EventHandler<MessageEventArgs> MessageDeleted = delegate { }; | |||
public event EventHandler<MessageUpdatedEventArgs> MessageUpdated = delegate { }; | |||
[Obsolete] | |||
public event EventHandler<MessageEventArgs> MessageReadRemotely = delegate { }; | |||
private readonly bool _useServerWhitelist, _useChannelWhitelist, _allowAll, _allowPrivate; | |||
@@ -99,10 +99,8 @@ namespace Discord.Modules | |||
client.ChannelUpdated += (s, e) => { if (HasChannel(e.After)) ChannelUpdated(s, e); }; | |||
client.MessageReceived += (s, e) => { if (HasChannel(e.Channel)) MessageReceived(s, e); }; | |||
client.MessageSent += (s, e) => { if (HasChannel(e.Channel)) MessageSent(s, e); }; | |||
client.MessageDeleted += (s, e) => { if (HasChannel(e.Channel)) MessageDeleted(s, e); }; | |||
client.MessageUpdated += (s, e) => { if (HasChannel(e.Channel)) MessageUpdated(s, e); }; | |||
client.MessageAcknowledged += (s, e) => { if (HasChannel(e.Channel)) MessageReadRemotely(s, e); }; | |||
client.RoleCreated += (s, e) => { if (HasIndirectServer(e.Server)) RoleCreated(s, e); }; | |||
client.RoleUpdated += (s, e) => { if (HasIndirectServer(e.Server)) RoleUpdated(s, e); }; | |||
@@ -185,7 +183,6 @@ namespace Discord.Modules | |||
public void DisableAllServers() | |||
{ | |||
if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist."); | |||
if (!_useServerWhitelist) return; | |||
using (_lock.Lock()) | |||
{ | |||
@@ -1,26 +1,39 @@ | |||
{ | |||
"version": "0.9.0-rc3", | |||
"version": "0.9.6", | |||
"description": "A Discord.Net extension adding basic plugin support.", | |||
"authors": [ "RogueException" ], | |||
"tags": [ "discord", "discordapp" ], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
"packOptions": { | |||
"tags": [ "discord", "discordapp" ], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
} | |||
}, | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"compilationOptions": { | |||
"buildOptions": { | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"warningsAsErrors": true | |||
}, | |||
"dependencies": { | |||
"Discord.Net": "0.9.0-rc3-3", | |||
"Discord.Net.Commands": "0.9.0-rc3-1" | |||
"Discord.Net": "0.9.6", | |||
"Discord.Net.Commands": "0.9.6" | |||
}, | |||
"frameworks": { | |||
"net45": { }, | |||
"dotnet5.4": { } | |||
"netstandard1.3": { | |||
"dependencies": { | |||
"NETStandard.Library": "1.6.0" | |||
}, | |||
"imports": [ | |||
"dotnet5.4", | |||
"dnxcore50", | |||
"portable-net45+win8" | |||
] | |||
}, | |||
"net45": {} | |||
} | |||
} |
@@ -70,6 +70,9 @@ | |||
<Compile Include="..\Discord.Net\API\Client\Common\ExtendedMember.cs"> | |||
<Link>API\Client\Common\ExtendedMember.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\Common\Game.cs"> | |||
<Link>API\Client\Common\Game.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\Common\Guild.cs"> | |||
<Link>API\Client\Common\Guild.cs</Link> | |||
</Compile> | |||
@@ -199,8 +202,8 @@ | |||
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Ready.cs"> | |||
<Link>API\Client\GatewaySocket\Events\Ready.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Redirect.cs"> | |||
<Link>API\Client\GatewaySocket\Events\Redirect.cs</Link> | |||
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Reconnect.cs"> | |||
<Link>API\Client\GatewaySocket\Events\Reconnect.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Resumed.cs"> | |||
<Link>API\Client\GatewaySocket\Events\Resumed.cs</Link> | |||
@@ -238,6 +241,9 @@ | |||
<Compile Include="..\Discord.Net\API\Client\Rest\AddGuildBan.cs"> | |||
<Link>API\Client\Rest\AddGuildBan.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\Rest\BulkMessageDelete.cs"> | |||
<Link>API\Client\Rest\BulkMessageDelete.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\Rest\CreateChannel.cs"> | |||
<Link>API\Client\Rest\CreateChannel.cs</Link> | |||
</Compile> | |||
@@ -337,6 +343,9 @@ | |||
<Compile Include="..\Discord.Net\API\Client\Rest\UpdateMessage.cs"> | |||
<Link>API\Client\Rest\UpdateMessage.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\Rest\UpdateOwnNick.cs"> | |||
<Link>API\Client\Rest\UpdateOwnNick.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Client\Rest\UpdateProfile.cs"> | |||
<Link>API\Client\Rest\UpdateProfile.cs</Link> | |||
</Compile> | |||
@@ -409,6 +418,9 @@ | |||
<Compile Include="..\Discord.Net\Enums\ConnectionState.cs"> | |||
<Link>Enums\ConnectionState.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Enums\GameType.cs"> | |||
<Link>Enums\GameType.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Enums\ImageType.cs"> | |||
<Link>Enums\ImageType.cs</Link> | |||
</Compile> | |||
@@ -430,6 +442,9 @@ | |||
<Compile Include="..\Discord.Net\Enums\StringEnum.cs"> | |||
<Link>Enums\StringEnum.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Enums\TokenType.cs"> | |||
<Link>Enums\TokenType.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Enums\UserStatus.cs"> | |||
<Link>Enums\UserStatus.cs</Link> | |||
</Compile> | |||
@@ -517,6 +532,9 @@ | |||
<Compile Include="..\Discord.Net\Models\Color.cs"> | |||
<Link>Models\Color.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Models\Game.cs"> | |||
<Link>Models\Game.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Models\Invite.cs"> | |||
<Link>Models\Invite.cs</Link> | |||
</Compile> | |||
@@ -547,9 +565,6 @@ | |||
<Compile Include="..\Discord.Net\Net\Rest\CompletedRequestEventArgs.cs"> | |||
<Link>Net\Rest\CompletedRequestEventArgs.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Net\Rest\ETFRestClient.cs"> | |||
<Link>Net\Rest\ETFRestClient.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Net\Rest\IRestEngine.cs"> | |||
<Link>Net\Rest\IRestEngine.cs</Link> | |||
</Compile> | |||
@@ -13,5 +13,5 @@ using System.Runtime.InteropServices; | |||
[assembly: ComVisible(false)] | |||
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | |||
[assembly: AssemblyVersion("0.9.0.0")] | |||
[assembly: AssemblyFileVersion("0.9.0.0")] | |||
[assembly: AssemblyVersion("0.9.6.0")] | |||
[assembly: AssemblyFileVersion("0.9.6.0")] |
@@ -53,15 +53,15 @@ namespace Discord | |||
} | |||
} | |||
public static async Task Wait(this CancellationTokenSource tokenSource) | |||
public static async Task Wait(this CancellationTokenSource cancelTokenSource) | |||
{ | |||
var token = tokenSource.Token; | |||
try { await Task.Delay(-1, token).ConfigureAwait(false); } | |||
var cancelToken = cancelTokenSource.Token; | |||
try { await Task.Delay(-1, cancelToken).ConfigureAwait(false); } | |||
catch (OperationCanceledException) { } //Expected | |||
} | |||
public static async Task Wait(this CancellationToken token) | |||
public static async Task Wait(this CancellationToken cancelToken) | |||
{ | |||
try { await Task.Delay(-1, token).ConfigureAwait(false); } | |||
try { await Task.Delay(-1, cancelToken).ConfigureAwait(false); } | |||
catch (OperationCanceledException) { } //Expected | |||
} | |||
} | |||
@@ -0,0 +1,14 @@ | |||
using Newtonsoft.Json; | |||
namespace Discord.API.Client | |||
{ | |||
public class Game | |||
{ | |||
[JsonProperty("name")] | |||
public string Name { get; set; } | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
[JsonProperty("type")] | |||
public GameType? Type { get; set; } | |||
} | |||
} |
@@ -10,5 +10,7 @@ namespace Discord.API.Client | |||
public DateTime? JoinedAt { get; set; } | |||
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] | |||
public ulong[] Roles { get; set; } | |||
[JsonProperty("nick")] | |||
public string Nick { get; set; } = ""; | |||
} | |||
} |
@@ -5,13 +5,8 @@ namespace Discord.API.Client | |||
{ | |||
public class MemberPresence : MemberReference | |||
{ | |||
public class GameInfo | |||
{ | |||
[JsonProperty("name")] | |||
public string Name { get; set; } | |||
} | |||
[JsonProperty("game")] | |||
public GameInfo Game { get; set; } | |||
public Game Game { get; set; } | |||
[JsonProperty("status")] | |||
public string Status { get; set; } | |||
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] | |||
@@ -19,5 +19,7 @@ namespace Discord.API.Client | |||
public uint? Color { get; set; } | |||
[JsonProperty("managed")] | |||
public bool? Managed { get; set; } | |||
[JsonProperty("mentionable")] | |||
public bool? Mentionable { get; set; } | |||
} | |||
} |
@@ -13,5 +13,7 @@ namespace Discord.API.Client | |||
public ushort? Discriminator { get; set; } | |||
[JsonProperty("avatar")] | |||
public string Avatar { get; set; } | |||
[JsonProperty("bot")] | |||
public bool? Bot { get; set; } | |||
} | |||
} |
@@ -10,8 +10,6 @@ namespace Discord.API.Client.GatewaySocket | |||
object IWebSocketMessage.Payload => this; | |||
bool IWebSocketMessage.IsPrivate => false; | |||
[JsonProperty("v")] | |||
public int Version { get; set; } | |||
[JsonProperty("token")] | |||
public string Token { get; set; } | |||
[JsonProperty("properties")] | |||
@@ -20,5 +18,7 @@ namespace Discord.API.Client.GatewaySocket | |||
public int LargeThreshold { get; set; } | |||
[JsonProperty("compress")] | |||
public bool UseCompression { get; set; } | |||
[JsonProperty("shard", NullValueHandling = NullValueHandling.Ignore)] | |||
public int[] ShardingParams { get; set; } | |||
} | |||
} |
@@ -9,15 +9,13 @@ namespace Discord.API.Client.GatewaySocket | |||
object IWebSocketMessage.Payload => this; | |||
bool IWebSocketMessage.IsPrivate => false; | |||
public class GameInfo | |||
{ | |||
[JsonProperty("name")] | |||
public string Name { get; set; } | |||
} | |||
[JsonProperty("idle_since")] | |||
[JsonProperty("afk")] | |||
public bool? Afk { get; set; } | |||
[JsonProperty("since")] | |||
public long? IdleSince { get; set; } | |||
[JsonProperty("game")] | |||
public GameInfo Game { get; set; } | |||
public Game Game { get; set; } | |||
[JsonProperty("status")] | |||
public string Status { get; set; } | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public class ReconnectEvent { } | |||
} |
@@ -1,10 +0,0 @@ | |||
using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public class RedirectEvent | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
} | |||
} |
@@ -17,8 +17,14 @@ | |||
/// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | |||
Resume = 6, | |||
/// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | |||
Redirect = 7, | |||
/// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | |||
RequestGuildMembers = 8 | |||
Reconnect = 7, | |||
/// <summary> C→S - Used to request all members that were withheld by large_threshold. </summary> | |||
RequestGuildMembers = 8, | |||
/// <summary> C←S - Used to notify the client of an invalid session id. </summary> | |||
InvalidSession = 9, | |||
/// <summary> C←S - Used to receive heartbeat_interval information and initiate the websocket connection. </summary> | |||
Hello = 10, | |||
/// <summary> C←S - Used to acknowledge a heartbeat by the client. </summary> | |||
HeartbeatACK = 11 | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
using Newtonsoft.Json; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public class BulkMessageDelete : IRestRequest | |||
{ | |||
string IRestRequest.Endpoint => $"/channels/{ChannelId}/messages/bulk_delete"; | |||
string IRestRequest.Method => "POST"; | |||
object IRestRequest.Payload => this; | |||
public ulong ChannelId { get; set; } | |||
public BulkMessageDelete(ulong channelId, ulong[] messageIds) | |||
{ | |||
ChannelId = channelId; | |||
MessageIds = messageIds; | |||
} | |||
[JsonProperty("messages")] | |||
public ulong[] MessageIds { get; set; } | |||
} | |||
} |
@@ -9,10 +9,10 @@ namespace Discord.API.Client.Rest | |||
string IRestRequest.Endpoint => $"gateway"; | |||
object IRestRequest.Payload => null; | |||
} | |||
public class GatewayResponse | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
} | |||
} |
@@ -1,5 +1,6 @@ | |||
using Discord.API.Converters; | |||
using Newtonsoft.Json; | |||
using System.ComponentModel; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
@@ -21,6 +22,8 @@ namespace Discord.API.Client.Rest | |||
public ulong? VoiceChannelId { get; set; } | |||
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] | |||
public ulong[] RoleIds { get; set; } | |||
[JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Nickname { get; set; } | |||
public UpdateMemberRequest(ulong guildId, ulong userId) | |||
{ | |||
@@ -0,0 +1,23 @@ | |||
using Newtonsoft.Json; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public class UpdateOwnNick : IRestRequest | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/members/@me/nick"; | |||
object IRestRequest.Payload => this; | |||
public ulong GuildId { get; set; } | |||
[JsonProperty("nick")] | |||
public string Nickname { get; set; } | |||
public UpdateOwnNick(ulong guildId, string nickname) | |||
{ | |||
GuildId = guildId; | |||
Nickname = nickname; | |||
} | |||
} | |||
} |
@@ -18,6 +18,8 @@ namespace Discord.API.Client.Rest | |||
public uint Permissions { get; set; } | |||
[JsonProperty("hoist")] | |||
public bool IsHoisted { get; set; } | |||
[JsonProperty("mentionable")] | |||
public bool IsMentionable { get; set; } | |||
[JsonProperty("color")] | |||
public uint Color { get; set; } | |||
@@ -4,12 +4,12 @@ | |||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> | |||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<PropertyGroup Label="Globals"> | |||
<ProjectGuid>acfb060b-ec8a-4926-b293-04c01e17ee23</ProjectGuid> | |||
<RootNamespace>Discord</RootNamespace> | |||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> | |||
</PropertyGroup> | |||
<PropertyGroup> | |||
<SchemaVersion>2.0</SchemaVersion> | |||
@@ -17,5 +17,5 @@ | |||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> | |||
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
</Project> |
@@ -7,42 +7,73 @@ namespace Discord | |||
{ | |||
public event EventHandler Ready = delegate { }; | |||
//public event EventHandler<DisconnectedEventArgs> LoggedOut = delegate { }; | |||
/// <summary>When a new channel is created, relevant to the current user. | |||
/// Discord API Event name: CHANNEL_CREATE.</summary> | |||
public event EventHandler<ChannelEventArgs> ChannelCreated = delegate { }; | |||
/// <summary>When a channel relevant to the current user is deleted. | |||
/// Discord API Event name: CHANNEL_DELETE.</summary> | |||
public event EventHandler<ChannelEventArgs> ChannelDestroyed = delegate { }; | |||
/// <summary>When a channel is updated. Discord API Event name: CHANNEL_UPDATE.</summary> | |||
public event EventHandler<ChannelUpdatedEventArgs> ChannelUpdated = delegate { }; | |||
[Obsolete] | |||
public event EventHandler<MessageEventArgs> MessageAcknowledged = delegate { }; | |||
/// <summary>When a message is deleted. Discord API Event name: MESSAGE_DELETE.</summary> | |||
public event EventHandler<MessageEventArgs> MessageDeleted = delegate { }; | |||
/// <summary>When a message is created. Discord API Event name: MESSAGE_CREATE.</summary> | |||
public event EventHandler<MessageEventArgs> MessageReceived = delegate { }; | |||
[Obsolete] | |||
public event EventHandler<MessageEventArgs> MessageSent = delegate { }; | |||
/// <summary>When a message is updated. Discord API Event name: MESSAGE_UPDATE.</summary> | |||
public event EventHandler<MessageUpdatedEventArgs> MessageUpdated = delegate { }; | |||
/// <summary>When properties about the user change. Discord API Event name: USER_UPDATE.</summary> | |||
public event EventHandler<ProfileUpdatedEventArgs> ProfileUpdated = delegate { }; | |||
/// <summary>When a server role is created. Discord API Event name: GUILD_ROLE_CREATE.</summary> | |||
public event EventHandler<RoleEventArgs> RoleCreated = delegate { }; | |||
/// <summary>When a server role is updated. Discord API Event name: GUILD_ROLE_UPDATE.</summary> | |||
public event EventHandler<RoleUpdatedEventArgs> RoleUpdated = delegate { }; | |||
/// <summary>When a server role is deleted. Discord API Event name: GUILD_ROLE_DELETE.</summary> | |||
public event EventHandler<RoleEventArgs> RoleDeleted = delegate { }; | |||
/// <summary>When the current user joins a new server. Discord API Event name: GUILD_CREATE.</summary> | |||
public event EventHandler<ServerEventArgs> JoinedServer = delegate { }; | |||
/// <summary>When the user leaves or is removed from a server. | |||
/// Discord API Event name: GUILD_DELETE.</summary> | |||
public event EventHandler<ServerEventArgs> LeftServer = delegate { }; | |||
/// <summary>When a server becomes available (current user is initially connecting, | |||
/// or a server becomes available again). | |||
/// Discord API Event name: GUILD_CREATE or GUILD_MEMBERS_CHUNK.</summary> | |||
public event EventHandler<ServerEventArgs> ServerAvailable = delegate { }; | |||
/// <summary>When a server is updated. Discord API Event name: GUILD_UPDATE.</summary> | |||
public event EventHandler<ServerUpdatedEventArgs> ServerUpdated = delegate { }; | |||
/// <summary>When a server becomes unavailable during a server outage. | |||
/// Discord API Event name: GUILD_DELETE.</summary> | |||
public event EventHandler<ServerEventArgs> ServerUnavailable = delegate { }; | |||
/// <summary>When a user is banned from a server. Discord API Event name: GUILD_BAN_ADD.</summary> | |||
public event EventHandler<UserEventArgs> UserBanned = delegate { }; | |||
/// <summary>When a user starts typing in a channel. Discord API Event name: TYPING_START.</summary> | |||
public event EventHandler<ChannelUserEventArgs> UserIsTyping = delegate { }; | |||
/// <summary>When a new user joins a server. Discord API Event name: GUILD_MEMBER_ADD.</summary> | |||
public event EventHandler<UserEventArgs> UserJoined = delegate { }; | |||
/// <summary>When a user is removed from a server (leave/kick/ban). | |||
/// Discord API Event name: GUILD_MEMBER_REMOVE.</summary> | |||
public event EventHandler<UserEventArgs> UserLeft = delegate { }; | |||
/// <summary>When a server member is updated. | |||
/// Discord API Event name: GUILD_MEMBER_UPDATE or PRESENCE_UPDATE or VOICE_STATE_UPDATE.</summary> | |||
public event EventHandler<UserUpdatedEventArgs> UserUpdated = delegate { }; | |||
/// <summary>When a user is unbanned from a server. Discord API Event name: GUILD_BAN_REMOVE.</summary> | |||
public event EventHandler<UserEventArgs> UserUnbanned = delegate { }; | |||
private void OnReady() | |||
=> OnEvent(Ready); | |||
/*private void OnLoggedOut(bool wasUnexpected, Exception ex) | |||
=> OnEvent(LoggedOut, new DisconnectedEventArgs(wasUnexpected, ex));*/ | |||
private void OnChannelCreated(Channel channel) | |||
=> OnEvent(ChannelCreated, new ChannelEventArgs(channel)); | |||
private void OnChannelDestroyed(Channel channel) | |||
=> OnEvent(ChannelDestroyed, new ChannelEventArgs(channel)); | |||
private void OnChannelUpdated(Channel before, Channel after) | |||
=> OnEvent(ChannelUpdated, new ChannelUpdatedEventArgs(before, after)); | |||
private void OnMessageAcknowledged(Message msg) | |||
=> OnEvent(MessageAcknowledged, new MessageEventArgs(msg)); | |||
private void OnMessageDeleted(Message msg) | |||
@@ -33,6 +33,7 @@ namespace Discord | |||
private ConcurrentDictionary<ulong, Channel> _privateChannels; //Key = RecipientId | |||
private Dictionary<string, Region> _regions; | |||
private Stopwatch _connectionStopwatch; | |||
private ConcurrentQueue<ulong> _largeServers; | |||
internal Logger Logger { get; } | |||
@@ -64,7 +65,7 @@ namespace Discord | |||
/// <summary> Gets the status of the current user. </summary> | |||
public UserStatus Status { get; private set; } | |||
/// <summary> Gets the game the current user is displayed as playing. </summary> | |||
public string CurrentGame { get; private set; } | |||
public Game CurrentGame { get; private set; } | |||
/// <summary> Gets a collection of all extensions added to this DiscordClient. </summary> | |||
public IEnumerable<IService> Services => _services; | |||
@@ -125,6 +126,7 @@ namespace Discord | |||
_servers = new ConcurrentDictionary<ulong, Server>(2, 0); | |||
_channels = new ConcurrentDictionary<ulong, Channel>(2, 0); | |||
_privateChannels = new ConcurrentDictionary<ulong, Channel>(2, 0); | |||
_largeServers = new ConcurrentQueue<ulong>(); | |||
//Serialization | |||
Serializer = new JsonSerializer(); | |||
@@ -149,7 +151,7 @@ namespace Discord | |||
//GatewaySocket.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception); | |||
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); | |||
MessageQueue = new MessageQueue(ClientAPI, Log.CreateLogger("MessageQueue")); | |||
//Extensibility | |||
@@ -167,14 +169,14 @@ namespace Discord | |||
return ClientAPI.Token; | |||
} | |||
/// <summary> Connects to the Discord server with the provided token. </summary> | |||
public async Task Connect(string token) | |||
public async Task Connect(string token, TokenType tokenType) | |||
{ | |||
if (token == null) throw new ArgumentNullException(token); | |||
await BeginConnect(null, null, token).ConfigureAwait(false); | |||
await BeginConnect(null, null, token, tokenType).ConfigureAwait(false); | |||
} | |||
private async Task BeginConnect(string email, string password, string token = null) | |||
private async Task BeginConnect(string email, string password, string token = null, TokenType tokenType = TokenType.User) | |||
{ | |||
try | |||
{ | |||
@@ -197,10 +199,21 @@ namespace Discord | |||
ClientAPI.CancelToken = CancelToken; | |||
StatusAPI.CancelToken = CancelToken; | |||
switch (tokenType) | |||
{ | |||
case TokenType.Bot: | |||
token = $"Bot {token}"; | |||
break; | |||
case TokenType.User: | |||
break; | |||
default: | |||
throw new ArgumentException("Unknown oauth token type", nameof(tokenType)); | |||
} | |||
await Login(email, password, token).ConfigureAwait(false); | |||
await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); | |||
var tasks = new[] { CancelToken.Wait() } | |||
var tasks = new[] { CancelToken.Wait(), LargeServerDownloader(CancelToken) } | |||
.Concat(MessageQueue.Run(CancelToken)); | |||
await _taskManager.Start(tasks, cancelSource).ConfigureAwait(false); | |||
@@ -241,12 +254,15 @@ namespace Discord | |||
} | |||
ClientAPI.Token = token; | |||
var request = new LoginRequest() { Email = email, Password = password }; | |||
var response = await ClientAPI.Send(request).ConfigureAwait(false); | |||
token = response.Token; | |||
if (Config.CacheDir != null && token != oldToken && tokenPath != null) | |||
SaveToken(tokenPath, cacheKey, token); | |||
ClientAPI.Token = token; | |||
if (email != null && password != null) | |||
{ | |||
var request = new LoginRequest() { Email = email, Password = password }; | |||
var response = await ClientAPI.Send(request).ConfigureAwait(false); | |||
token = response.Token; | |||
if (Config.CacheDir != null && token != oldToken && tokenPath != null) | |||
SaveToken(tokenPath, cacheKey, token); | |||
ClientAPI.Token = token; | |||
} | |||
//Cache other stuff | |||
var regionsResponse = (await ClientAPI.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false)); | |||
@@ -274,7 +290,7 @@ namespace Discord | |||
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | |||
public Task Disconnect() => _taskManager.Stop(true); | |||
private async Task Cleanup() | |||
private async Task Cleanup() | |||
{ | |||
var oldState = State; | |||
State = ConnectionState.Disconnecting; | |||
@@ -284,7 +300,10 @@ namespace Discord | |||
try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } | |||
catch (OperationCanceledException) { } | |||
} | |||
ulong serverId; | |||
while (_largeServers.TryDequeue(out serverId)) { } | |||
MessageQueue.Clear(); | |||
await GatewaySocket.Disconnect().ConfigureAwait(false); | |||
@@ -296,26 +315,36 @@ namespace Discord | |||
PrivateUser = null; | |||
CurrentUser = null; | |||
State = (int)ConnectionState.Disconnected; | |||
_connectedEvent.Reset(); | |||
_disconnectedEvent.Set(); | |||
} | |||
public void SetStatus(UserStatus 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)); | |||
if (status != UserStatus.Online && status != UserStatus.Idle && status != UserStatus.DoNotDisturb && status != UserStatus.Invisible) | |||
throw new ArgumentException($"Invalid status, must be {UserStatus.Online}, {UserStatus.Idle}, {UserStatus.DoNotDisturb} or {UserStatus.Invisible}", nameof(status)); | |||
Status = status; | |||
SendStatus(); | |||
} | |||
public void SetGame(string game) | |||
public void SetGame(Game game) | |||
{ | |||
CurrentGame = game; | |||
SendStatus(); | |||
} | |||
public void SetGame(string game) | |||
{ | |||
CurrentGame = new Game(game); | |||
SendStatus(); | |||
} | |||
public void SetGame(string game, GameType type, string url) | |||
{ | |||
CurrentGame = new Game(game, type, url); | |||
SendStatus(); | |||
} | |||
private void SendStatus() | |||
{ | |||
PrivateUser.Status = Status; | |||
@@ -331,7 +360,7 @@ namespace Discord | |||
} | |||
var socket = GatewaySocket; | |||
if (socket != null) | |||
socket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGame); | |||
socket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGame, Status == UserStatus.Idle, Status); | |||
} | |||
#region Channels | |||
@@ -488,15 +517,10 @@ namespace Discord | |||
var data = e.Payload.ToObject<ReadyEvent>(Serializer); | |||
int channelCount = 0; | |||
for (int i = 0; i < data.Guilds.Length; i++) | |||
channelCount += data.Guilds[i].Channels.Length; | |||
//ConcurrencyLevel = 2 (only REST and WebSocket can add/remove) | |||
_servers = new ConcurrentDictionary<ulong, Server>(2, (int)(data.Guilds.Length * 1.05)); | |||
_channels = new ConcurrentDictionary<ulong, Channel>(2, (int)(channelCount * 1.05)); | |||
_channels = new ConcurrentDictionary<ulong, Channel>(2, (int)(data.Guilds.Length * 2 * 1.05)); | |||
_privateChannels = new ConcurrentDictionary<ulong, Channel>(2, (int)(data.PrivateChannels.Length * 1.05)); | |||
List<ulong> largeServers = new List<ulong>(); | |||
SessionId = data.SessionId; | |||
PrivateUser = new User(this, data.User.Id, null); | |||
@@ -511,9 +535,9 @@ namespace Discord | |||
{ | |||
var server = AddServer(model.Id); | |||
server.Update(model); | |||
if (model.IsLarge) | |||
largeServers.Add(server.Id); | |||
} | |||
if (model.IsLarge) | |||
_largeServers.Enqueue(model.Id); | |||
} | |||
for (int i = 0; i < data.PrivateChannels.Length; i++) | |||
{ | |||
@@ -521,10 +545,8 @@ namespace Discord | |||
var channel = AddPrivateChannel(model.Id, model.Recipient.Id); | |||
channel.Update(model); | |||
} | |||
if (largeServers.Count > 0) | |||
GatewaySocket.SendRequestMembers(largeServers, "", 0); | |||
else | |||
EndConnect(); | |||
EndConnect(); | |||
} | |||
break; | |||
@@ -536,15 +558,19 @@ namespace Discord | |||
{ | |||
var server = AddServer(data.Id); | |||
server.Update(data); | |||
if (data.Unavailable != false) | |||
{ | |||
Logger.Info($"GUILD_CREATE: {server.Path}"); | |||
OnJoinedServer(server); | |||
} | |||
else | |||
Logger.Info($"GUILD_AVAILABLE: {server.Path}"); | |||
if (data.Unavailable != false) | |||
OnJoinedServer(server); | |||
OnServerAvailable(server); | |||
if (!data.IsLarge) | |||
OnServerAvailable(server); | |||
else | |||
_largeServers.Enqueue(data.Id); | |||
} | |||
} | |||
break; | |||
@@ -707,16 +733,7 @@ namespace Discord | |||
Logger.Verbose($"GUILD_MEMBERS_CHUNK: {data.Members.Length} users"); | |||
if (server.CurrentUserCount >= server.UserCount) //Finished downloading for there | |||
{ | |||
bool isConnectComplete = true; | |||
foreach (var server2 in _servers.Select(x => x.Value)) | |||
{ | |||
if (server2.CurrentUserCount < server2.UserCount) | |||
isConnectComplete = false; | |||
} | |||
if (isConnectComplete) | |||
EndConnect(); | |||
} | |||
OnServerAvailable(server); | |||
} | |||
else | |||
Logger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); | |||
@@ -853,7 +870,7 @@ namespace Discord | |||
msg.Update(data); | |||
user.UpdateActivity(); | |||
Logger.Verbose($"MESSAGE_CREATE: {channel.Path} ({user.Name ?? "Unknown"})"); | |||
OnMessageReceived(msg); | |||
} | |||
@@ -1046,11 +1063,6 @@ namespace Discord | |||
asyncAction().GetAwaiter().GetResult(); | |||
_disconnectedEvent.WaitOne(); | |||
} | |||
/// <summary> Blocking call and wait until the client has been manually stopped. This is mainly intended for use in console applications. </summary> | |||
public void Wait() | |||
{ | |||
_disconnectedEvent.WaitOne(); | |||
} | |||
#endregion | |||
#region IDisposable | |||
@@ -1068,13 +1080,44 @@ namespace Discord | |||
_isDisposed = true; | |||
} | |||
} | |||
public void Dispose() | |||
{ | |||
Dispose(true); | |||
} | |||
#endregion | |||
private Task LargeServerDownloader(CancellationToken cancelToken) | |||
{ | |||
//Temporary hotfix to download all large guilds before raising READY | |||
return Task.Run(async () => | |||
{ | |||
try | |||
{ | |||
const short batchSize = 50; | |||
ulong[] serverIds = new ulong[batchSize]; | |||
while (!cancelToken.IsCancellationRequested && State == ConnectionState.Connecting) | |||
await Task.Delay(1000, cancelToken).ConfigureAwait(false); | |||
while (!cancelToken.IsCancellationRequested && State == ConnectionState.Connected) | |||
{ | |||
if (_largeServers.Count > 0) | |||
{ | |||
int count = 0; | |||
while (count < batchSize && _largeServers.TryDequeue(out serverIds[count])) | |||
count++; | |||
if (count > 0) | |||
GatewaySocket.SendRequestMembers(serverIds.Take(count), "", 0); | |||
} | |||
await Task.Delay(1250, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
catch (OperationCanceledException) { } | |||
}); | |||
} | |||
//Helpers | |||
private string GetTokenCachePath(string email) | |||
{ | |||
@@ -47,6 +47,11 @@ namespace Discord | |||
/// </summary> | |||
public int LargeThreshold { get; set; } = 250; | |||
/// <summary> Gets or sets the id for this shard. Must be less than TotalShards. </summary> | |||
public int ShardId { get; set; } = 0; | |||
/// <summary> Gets or sets the total number of shards for this application. </summary> | |||
public int TotalShards { get; set; } = 1; | |||
//Events | |||
/// <summary> Gets or sets a handler for all log messages. </summary> | |||
@@ -61,6 +66,7 @@ namespace Discord | |||
internal const int RestTimeout = 10000; | |||
internal const int MessageQueueInterval = 100; | |||
internal const int WebSocketQueueInterval = 100; | |||
internal const int ServerBatchCount = 50; | |||
public const string LibName = "Discord.Net"; | |||
public static string LibVersion => typeof(DiscordConfigBuilder).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||
@@ -88,6 +94,8 @@ namespace Discord | |||
public bool UsePermissionsCache { get; } | |||
public bool EnablePreUpdateEvents { get; } | |||
public int ShardId { get; } | |||
public int TotalShards { get; } | |||
internal DiscordConfig(DiscordConfigBuilder builder) | |||
{ | |||
@@ -106,6 +114,8 @@ namespace Discord | |||
MessageCacheSize = builder.MessageCacheSize; | |||
UsePermissionsCache = builder.UsePermissionsCache; | |||
EnablePreUpdateEvents = builder.EnablePreUpdateEvents; | |||
ShardId = builder.ShardId; | |||
TotalShards = builder.TotalShards; | |||
} | |||
private static string GetUserAgent(DiscordConfigBuilder builder) | |||
@@ -1,491 +1,491 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Reflection.Emit; | |||
using System.Text; | |||
//using Newtonsoft.Json; | |||
//using System; | |||
//using System.Collections.Concurrent; | |||
//using System.Collections.Generic; | |||
//using System.IO; | |||
//using System.Linq; | |||
//using System.Reflection; | |||
//using System.Reflection.Emit; | |||
//using System.Text; | |||
namespace Discord.ETF | |||
{ | |||
public class ETFReader : IDisposable | |||
{ | |||
private static readonly ConcurrentDictionary<Type, Delegate> _deserializers = new ConcurrentDictionary<Type, Delegate>(); | |||
private static readonly Dictionary<Type, MethodInfo> _readMethods = GetPrimitiveReadMethods(); | |||
//namespace Discord.ETF | |||
//{ | |||
// public class ETFReader : IDisposable | |||
// { | |||
// private static readonly ConcurrentDictionary<Type, Delegate> _deserializers = new ConcurrentDictionary<Type, Delegate>(); | |||
// private static readonly Dictionary<Type, MethodInfo> _readMethods = GetPrimitiveReadMethods(); | |||
private readonly Stream _stream; | |||
private readonly byte[] _buffer; | |||
private readonly bool _leaveOpen; | |||
private readonly Encoding _encoding; | |||
// private readonly Stream _stream; | |||
// private readonly byte[] _buffer; | |||
// private readonly bool _leaveOpen; | |||
// private readonly Encoding _encoding; | |||
public ETFReader(Stream stream, bool leaveOpen = false) | |||
{ | |||
if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
// public ETFReader(Stream stream, bool leaveOpen = false) | |||
// { | |||
// if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
_stream = stream; | |||
_leaveOpen = leaveOpen; | |||
_buffer = new byte[11]; | |||
_encoding = Encoding.UTF8; | |||
} | |||
// _stream = stream; | |||
// _leaveOpen = leaveOpen; | |||
// _buffer = new byte[11]; | |||
// _encoding = Encoding.UTF8; | |||
// } | |||
public bool ReadBool() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT) | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
switch (_buffer[0]) //Length | |||
{ | |||
case 4: | |||
ReadTrue(); | |||
return true; | |||
case 5: | |||
ReadFalse(); | |||
return false; | |||
} | |||
} | |||
throw new InvalidDataException(); | |||
} | |||
private void ReadTrue() | |||
{ | |||
_stream.Read(_buffer, 0, 4); | |||
if (_buffer[0] != 't' || _buffer[1] != 'r' || _buffer[2] != 'u' || _buffer[3] != 'e') | |||
throw new InvalidDataException(); | |||
} | |||
private void ReadFalse() | |||
{ | |||
_stream.Read(_buffer, 0, 5); | |||
if (_buffer[0] != 'f' || _buffer[1] != 'a' || _buffer[2] != 'l' || _buffer[3] != 's' || _buffer[4] != 'e') | |||
throw new InvalidDataException(); | |||
} | |||
// public bool ReadBool() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT) | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// switch (_buffer[0]) //Length | |||
// { | |||
// case 4: | |||
// ReadTrue(); | |||
// return true; | |||
// case 5: | |||
// ReadFalse(); | |||
// return false; | |||
// } | |||
// } | |||
// throw new InvalidDataException(); | |||
// } | |||
// private void ReadTrue() | |||
// { | |||
// _stream.Read(_buffer, 0, 4); | |||
// if (_buffer[0] != 't' || _buffer[1] != 'r' || _buffer[2] != 'u' || _buffer[3] != 'e') | |||
// throw new InvalidDataException(); | |||
// } | |||
// private void ReadFalse() | |||
// { | |||
// _stream.Read(_buffer, 0, 5); | |||
// if (_buffer[0] != 'f' || _buffer[1] != 'a' || _buffer[2] != 'l' || _buffer[3] != 's' || _buffer[4] != 'e') | |||
// throw new InvalidDataException(); | |||
// } | |||
public int ReadSByte() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (sbyte)ReadLongInternal(type); | |||
} | |||
public uint ReadByte() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (byte)ReadLongInternal(type); | |||
} | |||
public int ReadShort() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (short)ReadLongInternal(type); | |||
} | |||
public uint ReadUShort() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (ushort)ReadLongInternal(type); | |||
} | |||
public int ReadInt() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (int)ReadLongInternal(type); | |||
} | |||
public uint ReadUInt() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (uint)ReadLongInternal(type); | |||
} | |||
public long ReadLong() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return ReadLongInternal(type); | |||
} | |||
public ulong ReadULong() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (ulong)ReadLongInternal(type); | |||
} | |||
public long ReadLongInternal(ETFType type) | |||
{ | |||
switch (type) | |||
{ | |||
case ETFType.SMALL_INTEGER_EXT: | |||
_stream.Read(_buffer, 0, 1); | |||
return _buffer[0]; | |||
case ETFType.INTEGER_EXT: | |||
_stream.Read(_buffer, 0, 4); | |||
return (_buffer[0] << 24) | (_buffer[1] << 16) | (_buffer[2] << 8) | (_buffer[3]); | |||
case ETFType.SMALL_BIG_EXT: | |||
_stream.Read(_buffer, 0, 2); | |||
bool isPositive = _buffer[0] == 0; | |||
byte count = _buffer[1]; | |||
// public int ReadSByte() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (sbyte)ReadLongInternal(type); | |||
// } | |||
// public uint ReadByte() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (byte)ReadLongInternal(type); | |||
// } | |||
// public int ReadShort() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (short)ReadLongInternal(type); | |||
// } | |||
// public uint ReadUShort() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (ushort)ReadLongInternal(type); | |||
// } | |||
// public int ReadInt() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (int)ReadLongInternal(type); | |||
// } | |||
// public uint ReadUInt() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (uint)ReadLongInternal(type); | |||
// } | |||
// public long ReadLong() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return ReadLongInternal(type); | |||
// } | |||
// public ulong ReadULong() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (ulong)ReadLongInternal(type); | |||
// } | |||
// public long ReadLongInternal(ETFType type) | |||
// { | |||
// switch (type) | |||
// { | |||
// case ETFType.SMALL_INTEGER_EXT: | |||
// _stream.Read(_buffer, 0, 1); | |||
// return _buffer[0]; | |||
// case ETFType.INTEGER_EXT: | |||
// _stream.Read(_buffer, 0, 4); | |||
// return (_buffer[0] << 24) | (_buffer[1] << 16) | (_buffer[2] << 8) | (_buffer[3]); | |||
// case ETFType.SMALL_BIG_EXT: | |||
// _stream.Read(_buffer, 0, 2); | |||
// bool isPositive = _buffer[0] == 0; | |||
// byte count = _buffer[1]; | |||
int shiftValue = (count - 1) * 8; | |||
ulong value = 0; | |||
_stream.Read(_buffer, 0, count); | |||
for (int i = 0; i < count; i++, shiftValue -= 8) | |||
value = value + _buffer[i] << shiftValue; | |||
if (!isPositive) | |||
return -(long)value; | |||
else | |||
return (long)value; | |||
} | |||
throw new InvalidDataException(); | |||
} | |||
// int shiftValue = (count - 1) * 8; | |||
// ulong value = 0; | |||
// _stream.Read(_buffer, 0, count); | |||
// for (int i = 0; i < count; i++, shiftValue -= 8) | |||
// value = value + _buffer[i] << shiftValue; | |||
// if (!isPositive) | |||
// return -(long)value; | |||
// else | |||
// return (long)value; | |||
// } | |||
// throw new InvalidDataException(); | |||
// } | |||
public float ReadSingle() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return (float)ReadDoubleInternal(type); | |||
} | |||
public double ReadDouble() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
return ReadDoubleInternal(type); | |||
} | |||
public double ReadDoubleInternal(ETFType type) | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
// public float ReadSingle() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return (float)ReadDoubleInternal(type); | |||
// } | |||
// public double ReadDouble() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// return ReadDoubleInternal(type); | |||
// } | |||
// public double ReadDoubleInternal(ETFType type) | |||
// { | |||
// throw new NotImplementedException(); | |||
// } | |||
public bool? ReadNullableBool() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT) | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
switch (_buffer[0]) //Length | |||
{ | |||
case 3: | |||
if (ReadNil()) | |||
return null; | |||
break; | |||
case 4: | |||
ReadTrue(); | |||
return true; | |||
case 5: | |||
ReadFalse(); | |||
return false; | |||
} | |||
} | |||
throw new InvalidDataException(); | |||
} | |||
public int? ReadNullableSByte() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (sbyte)ReadLongInternal(type); | |||
} | |||
public uint? ReadNullableByte() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (byte)ReadLongInternal(type); | |||
} | |||
public int? ReadNullableShort() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (short)ReadLongInternal(type); | |||
} | |||
public uint? ReadNullableUShort() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (ushort)ReadLongInternal(type); | |||
} | |||
public int? ReadNullableInt() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (int)ReadLongInternal(type); | |||
} | |||
public uint? ReadNullableUInt() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (uint)ReadLongInternal(type); | |||
} | |||
public long? ReadNullableLong() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return ReadLongInternal(type); | |||
} | |||
public ulong? ReadNullableULong() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (ulong)ReadLongInternal(type); | |||
} | |||
public float? ReadNullableSingle() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return (float)ReadDoubleInternal(type); | |||
} | |||
public double? ReadNullableDouble() | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
ETFType type = (ETFType)_buffer[0]; | |||
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
return ReadDoubleInternal(type); | |||
} | |||
// public bool? ReadNullableBool() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT) | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// switch (_buffer[0]) //Length | |||
// { | |||
// case 3: | |||
// if (ReadNil()) | |||
// return null; | |||
// break; | |||
// case 4: | |||
// ReadTrue(); | |||
// return true; | |||
// case 5: | |||
// ReadFalse(); | |||
// return false; | |||
// } | |||
// } | |||
// throw new InvalidDataException(); | |||
// } | |||
// public int? ReadNullableSByte() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (sbyte)ReadLongInternal(type); | |||
// } | |||
// public uint? ReadNullableByte() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (byte)ReadLongInternal(type); | |||
// } | |||
// public int? ReadNullableShort() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (short)ReadLongInternal(type); | |||
// } | |||
// public uint? ReadNullableUShort() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (ushort)ReadLongInternal(type); | |||
// } | |||
// public int? ReadNullableInt() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (int)ReadLongInternal(type); | |||
// } | |||
// public uint? ReadNullableUInt() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (uint)ReadLongInternal(type); | |||
// } | |||
// public long? ReadNullableLong() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return ReadLongInternal(type); | |||
// } | |||
// public ulong? ReadNullableULong() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (ulong)ReadLongInternal(type); | |||
// } | |||
// public float? ReadNullableSingle() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return (float)ReadDoubleInternal(type); | |||
// } | |||
// public double? ReadNullableDouble() | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// ETFType type = (ETFType)_buffer[0]; | |||
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; | |||
// return ReadDoubleInternal(type); | |||
// } | |||
public string ReadString() | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
public byte[] ReadByteArray() | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
// public string ReadString() | |||
// { | |||
// throw new NotImplementedException(); | |||
// } | |||
// public byte[] ReadByteArray() | |||
// { | |||
// throw new NotImplementedException(); | |||
// } | |||
public T Read<T>() | |||
where T : new() | |||
{ | |||
var type = typeof(T); | |||
var typeInfo = type.GetTypeInfo(); | |||
var action = _deserializers.GetOrAdd(type, _ => CreateDeserializer<T>(type, typeInfo)) as Func<ETFReader, T>; | |||
return action(this); | |||
} | |||
/*public void Read<T, U>() | |||
where T : Nullable<T> | |||
where U : struct, new() | |||
{ | |||
}*/ | |||
public T[] ReadArray<T>() | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
public IDictionary<TKey, TValue> ReadDictionary<TKey, TValue>() | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
/*public object Read(object obj) | |||
{ | |||
throw new NotImplementedException(); | |||
}*/ | |||
// public T Read<T>() | |||
// where T : new() | |||
// { | |||
// var type = typeof(T); | |||
// var typeInfo = type.GetTypeInfo(); | |||
// var action = _deserializers.GetOrAdd(type, _ => CreateDeserializer<T>(type, typeInfo)) as Func<ETFReader, T>; | |||
// return action(this); | |||
// } | |||
// /*public void Read<T, U>() | |||
// where T : Nullable<T> | |||
// where U : struct, new() | |||
// { | |||
// }*/ | |||
// public T[] ReadArray<T>() | |||
// { | |||
// throw new NotImplementedException(); | |||
// } | |||
// public IDictionary<TKey, TValue> ReadDictionary<TKey, TValue>() | |||
// { | |||
// throw new NotImplementedException(); | |||
// } | |||
// /*public object Read(object obj) | |||
// { | |||
// throw new NotImplementedException(); | |||
// }*/ | |||
private bool ReadNil(bool ignoreLength = false) | |||
{ | |||
if (!ignoreLength) | |||
{ | |||
_stream.Read(_buffer, 0, 1); | |||
byte length = _buffer[0]; | |||
if (length != 3) return false; | |||
} | |||
// private bool ReadNil(bool ignoreLength = false) | |||
// { | |||
// if (!ignoreLength) | |||
// { | |||
// _stream.Read(_buffer, 0, 1); | |||
// byte length = _buffer[0]; | |||
// if (length != 3) return false; | |||
// } | |||
_stream.Read(_buffer, 0, 3); | |||
if (_buffer[0] == 'n' && _buffer[1] == 'i' && _buffer[2] == 'l') | |||
return true; | |||
// _stream.Read(_buffer, 0, 3); | |||
// if (_buffer[0] == 'n' && _buffer[1] == 'i' && _buffer[2] == 'l') | |||
// return true; | |||
return false; | |||
} | |||
// return false; | |||
// } | |||
#region Emit | |||
private static Func<ETFReader, T> CreateDeserializer<T>(Type type, TypeInfo typeInfo) | |||
where T : new() | |||
{ | |||
var method = new DynamicMethod("DeserializeETF", type, new[] { typeof(ETFReader) }, true); | |||
var generator = method.GetILGenerator(); | |||
// #region Emit | |||
// private static Func<ETFReader, T> CreateDeserializer<T>(Type type, TypeInfo typeInfo) | |||
// where T : new() | |||
// { | |||
// var method = new DynamicMethod("DeserializeETF", type, new[] { typeof(ETFReader) }, true); | |||
// var generator = method.GetILGenerator(); | |||
generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
EmitReadValue(generator, type, typeInfo, true); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
// EmitReadValue(generator, type, typeInfo, true); | |||
generator.Emit(OpCodes.Ret); | |||
return method.CreateDelegate(typeof(Func<ETFReader, T>)) as Func<ETFReader, T>; | |||
} | |||
private static void EmitReadValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) | |||
{ | |||
//Convert enum types to their base type | |||
if (typeInfo.IsEnum) | |||
{ | |||
type = Enum.GetUnderlyingType(type); | |||
typeInfo = type.GetTypeInfo(); | |||
} | |||
//Primitives/Enums | |||
if (!typeInfo.IsEnum && IsType(type, typeof(sbyte), typeof(byte), typeof(short), | |||
typeof(ushort), typeof(int), typeof(uint), typeof(long), | |||
typeof(ulong), typeof(double), typeof(bool), typeof(string), | |||
typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), | |||
typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), | |||
typeof(bool?), typeof(float?), typeof(double?) | |||
/*typeof(object), typeof(DateTime)*/)) | |||
{ | |||
//No conversion needed | |||
generator.EmitCall(OpCodes.Call, GetReadMethod(type), null); | |||
} | |||
//Dictionaries | |||
/*else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) | |||
{ | |||
generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
//Enumerable | |||
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) | |||
{ | |||
generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
//Nullable Structs | |||
else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && | |||
typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) | |||
{ | |||
generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
//Structs/Classes | |||
else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) | |||
{ | |||
if (isTop) | |||
{ | |||
typeInfo.ForEachField(f => | |||
{ | |||
string name; | |||
if (!f.IsPublic || !IsETFProperty(f, out name)) return; | |||
// generator.Emit(OpCodes.Ret); | |||
// return method.CreateDelegate(typeof(Func<ETFReader, T>)) as Func<ETFReader, T>; | |||
// } | |||
// private static void EmitReadValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) | |||
// { | |||
// //Convert enum types to their base type | |||
// if (typeInfo.IsEnum) | |||
// { | |||
// type = Enum.GetUnderlyingType(type); | |||
// typeInfo = type.GetTypeInfo(); | |||
// } | |||
// //Primitives/Enums | |||
// if (!typeInfo.IsEnum && IsType(type, typeof(sbyte), typeof(byte), typeof(short), | |||
// typeof(ushort), typeof(int), typeof(uint), typeof(long), | |||
// typeof(ulong), typeof(double), typeof(bool), typeof(string), | |||
// typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), | |||
// typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), | |||
// typeof(bool?), typeof(float?), typeof(double?) | |||
// /*typeof(object), typeof(DateTime)*/)) | |||
// { | |||
// //No conversion needed | |||
// generator.EmitCall(OpCodes.Call, GetReadMethod(type), null); | |||
// } | |||
// //Dictionaries | |||
// /*else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) | |||
// { | |||
// generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// //Enumerable | |||
// else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) | |||
// { | |||
// generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// //Nullable Structs | |||
// else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && | |||
// typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) | |||
// { | |||
// generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// //Structs/Classes | |||
// else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) | |||
// { | |||
// if (isTop) | |||
// { | |||
// typeInfo.ForEachField(f => | |||
// { | |||
// string name; | |||
// if (!f.IsPublic || !IsETFProperty(f, out name)) return; | |||
generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name | |||
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj | |||
generator.Emit(OpCodes.Ldfld, f); //ETFReader(this), obj.fieldValue | |||
EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); | |||
}); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
// generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name | |||
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
// generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj | |||
// generator.Emit(OpCodes.Ldfld, f); //ETFReader(this), obj.fieldValue | |||
// EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); | |||
// }); | |||
typeInfo.ForEachProperty(p => | |||
{ | |||
string name; | |||
if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; | |||
// typeInfo.ForEachProperty(p => | |||
// { | |||
// string name; | |||
// if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; | |||
generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name | |||
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj | |||
generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFReader(this), obj.propValue | |||
EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); | |||
}); | |||
} | |||
else | |||
{ | |||
//While we could drill deeper and make a large serializer that also serializes all subclasses, | |||
//it's more efficient to serialize on a per-type basis via another Write<T> call. | |||
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
}*/ | |||
//Unsupported (decimal, char) | |||
else | |||
throw new InvalidOperationException($"Deserializing {type.Name} is not supported."); | |||
} | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
// generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name | |||
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) | |||
// generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj | |||
// generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFReader(this), obj.propValue | |||
// EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); | |||
// }); | |||
// } | |||
// else | |||
// { | |||
// //While we could drill deeper and make a large serializer that also serializes all subclasses, | |||
// //it's more efficient to serialize on a per-type basis via another Write<T> call. | |||
// generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// }*/ | |||
// //Unsupported (decimal, char) | |||
// else | |||
// throw new InvalidOperationException($"Deserializing {type.Name} is not supported."); | |||
// } | |||
private static bool IsType(Type type, params Type[] types) | |||
{ | |||
for (int i = 0; i < types.Length; i++) | |||
{ | |||
if (type == types[i]) | |||
return true; | |||
} | |||
return false; | |||
} | |||
private static bool IsETFProperty(FieldInfo f, out string name) | |||
{ | |||
var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
if (attrib != null) | |||
{ | |||
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name; | |||
return true; | |||
} | |||
name = null; | |||
return false; | |||
} | |||
private static bool IsETFProperty(PropertyInfo p, out string name) | |||
{ | |||
var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
if (attrib != null) | |||
{ | |||
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name; | |||
return true; | |||
} | |||
name = null; | |||
return false; | |||
} | |||
// private static bool IsType(Type type, params Type[] types) | |||
// { | |||
// for (int i = 0; i < types.Length; i++) | |||
// { | |||
// if (type == types[i]) | |||
// return true; | |||
// } | |||
// return false; | |||
// } | |||
// private static bool IsETFProperty(FieldInfo f, out string name) | |||
// { | |||
// var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
// if (attrib != null) | |||
// { | |||
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name; | |||
// return true; | |||
// } | |||
// name = null; | |||
// return false; | |||
// } | |||
// private static bool IsETFProperty(PropertyInfo p, out string name) | |||
// { | |||
// var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
// if (attrib != null) | |||
// { | |||
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name; | |||
// return true; | |||
// } | |||
// name = null; | |||
// return false; | |||
// } | |||
private static MethodInfo GetReadMethod(string name) | |||
=> typeof(ETFReader).GetTypeInfo().GetDeclaredMethods(name).Single(); | |||
private static MethodInfo GetReadMethod(Type type) | |||
{ | |||
MethodInfo method; | |||
if (_readMethods.TryGetValue(type, out method)) | |||
return method; | |||
return null; | |||
} | |||
private static Dictionary<Type, MethodInfo> GetPrimitiveReadMethods() | |||
{ | |||
return new Dictionary<Type, MethodInfo> | |||
{ | |||
{ typeof(bool), GetReadMethod(nameof(ReadBool)) }, | |||
{ typeof(bool?), GetReadMethod(nameof(ReadNullableBool)) }, | |||
{ typeof(byte), GetReadMethod(nameof(ReadByte)) }, | |||
{ typeof(byte?), GetReadMethod(nameof(ReadNullableByte)) }, | |||
{ typeof(sbyte), GetReadMethod(nameof(ReadSByte)) }, | |||
{ typeof(sbyte?), GetReadMethod(nameof(ReadNullableSByte)) }, | |||
{ typeof(short), GetReadMethod(nameof(ReadShort)) }, | |||
{ typeof(short?), GetReadMethod(nameof(ReadNullableShort)) }, | |||
{ typeof(ushort), GetReadMethod(nameof(ReadUShort)) }, | |||
{ typeof(ushort?), GetReadMethod(nameof(ReadNullableUShort)) }, | |||
{ typeof(int), GetReadMethod(nameof(ReadInt)) }, | |||
{ typeof(int?), GetReadMethod(nameof(ReadNullableInt)) }, | |||
{ typeof(uint), GetReadMethod(nameof(ReadUInt)) }, | |||
{ typeof(uint?), GetReadMethod(nameof(ReadNullableUInt)) }, | |||
{ typeof(long), GetReadMethod(nameof(ReadLong)) }, | |||
{ typeof(long?), GetReadMethod(nameof(ReadNullableLong)) }, | |||
{ typeof(ulong), GetReadMethod(nameof(ReadULong)) }, | |||
{ typeof(ulong?), GetReadMethod(nameof(ReadNullableULong)) }, | |||
{ typeof(float), GetReadMethod(nameof(ReadSingle)) }, | |||
{ typeof(float?), GetReadMethod(nameof(ReadNullableSingle)) }, | |||
{ typeof(double), GetReadMethod(nameof(ReadDouble)) }, | |||
{ typeof(double?), GetReadMethod(nameof(ReadNullableDouble)) }, | |||
}; | |||
} | |||
#endregion | |||
// private static MethodInfo GetReadMethod(string name) | |||
// => typeof(ETFReader).GetTypeInfo().GetDeclaredMethods(name).Single(); | |||
// private static MethodInfo GetReadMethod(Type type) | |||
// { | |||
// MethodInfo method; | |||
// if (_readMethods.TryGetValue(type, out method)) | |||
// return method; | |||
// return null; | |||
// } | |||
// private static Dictionary<Type, MethodInfo> GetPrimitiveReadMethods() | |||
// { | |||
// return new Dictionary<Type, MethodInfo> | |||
// { | |||
// { typeof(bool), GetReadMethod(nameof(ReadBool)) }, | |||
// { typeof(bool?), GetReadMethod(nameof(ReadNullableBool)) }, | |||
// { typeof(byte), GetReadMethod(nameof(ReadByte)) }, | |||
// { typeof(byte?), GetReadMethod(nameof(ReadNullableByte)) }, | |||
// { typeof(sbyte), GetReadMethod(nameof(ReadSByte)) }, | |||
// { typeof(sbyte?), GetReadMethod(nameof(ReadNullableSByte)) }, | |||
// { typeof(short), GetReadMethod(nameof(ReadShort)) }, | |||
// { typeof(short?), GetReadMethod(nameof(ReadNullableShort)) }, | |||
// { typeof(ushort), GetReadMethod(nameof(ReadUShort)) }, | |||
// { typeof(ushort?), GetReadMethod(nameof(ReadNullableUShort)) }, | |||
// { typeof(int), GetReadMethod(nameof(ReadInt)) }, | |||
// { typeof(int?), GetReadMethod(nameof(ReadNullableInt)) }, | |||
// { typeof(uint), GetReadMethod(nameof(ReadUInt)) }, | |||
// { typeof(uint?), GetReadMethod(nameof(ReadNullableUInt)) }, | |||
// { typeof(long), GetReadMethod(nameof(ReadLong)) }, | |||
// { typeof(long?), GetReadMethod(nameof(ReadNullableLong)) }, | |||
// { typeof(ulong), GetReadMethod(nameof(ReadULong)) }, | |||
// { typeof(ulong?), GetReadMethod(nameof(ReadNullableULong)) }, | |||
// { typeof(float), GetReadMethod(nameof(ReadSingle)) }, | |||
// { typeof(float?), GetReadMethod(nameof(ReadNullableSingle)) }, | |||
// { typeof(double), GetReadMethod(nameof(ReadDouble)) }, | |||
// { typeof(double?), GetReadMethod(nameof(ReadNullableDouble)) }, | |||
// }; | |||
// } | |||
// #endregion | |||
#region IDisposable | |||
private bool _isDisposed = false; | |||
// #region IDisposable | |||
// private bool _isDisposed = false; | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (!_isDisposed) | |||
{ | |||
if (disposing) | |||
{ | |||
if (_leaveOpen) | |||
_stream.Flush(); | |||
else | |||
_stream.Dispose(); | |||
} | |||
_isDisposed = true; | |||
} | |||
} | |||
// protected virtual void Dispose(bool disposing) | |||
// { | |||
// if (!_isDisposed) | |||
// { | |||
// if (disposing) | |||
// { | |||
// if (_leaveOpen) | |||
// _stream.Flush(); | |||
// else | |||
// _stream.Dispose(); | |||
// } | |||
// _isDisposed = true; | |||
// } | |||
// } | |||
public void Dispose() => Dispose(true); | |||
#endregion | |||
} | |||
} | |||
// public void Dispose() => Dispose(true); | |||
// #endregion | |||
// } | |||
//} |
@@ -1,482 +1,482 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Reflection.Emit; | |||
using System.Text; | |||
//using Newtonsoft.Json; | |||
//using System; | |||
//using System.Collections.Concurrent; | |||
//using System.Collections.Generic; | |||
//using System.IO; | |||
//using System.Linq; | |||
//using System.Reflection; | |||
//using System.Reflection.Emit; | |||
//using System.Text; | |||
namespace Discord.ETF | |||
{ | |||
public unsafe class ETFWriter : IDisposable | |||
{ | |||
private static readonly ConcurrentDictionary<Type, Delegate> _serializers = new ConcurrentDictionary<Type, Delegate>(); | |||
private static readonly ConcurrentDictionary<Type, Delegate> _indirectSerializers = new ConcurrentDictionary<Type, Delegate>(); | |||
//namespace Discord.ETF | |||
//{ | |||
// public unsafe class ETFWriter : IDisposable | |||
// { | |||
// private static readonly ConcurrentDictionary<Type, Delegate> _serializers = new ConcurrentDictionary<Type, Delegate>(); | |||
// private static readonly ConcurrentDictionary<Type, Delegate> _indirectSerializers = new ConcurrentDictionary<Type, Delegate>(); | |||
private static readonly byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' }; | |||
private static readonly byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; | |||
private static readonly byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' }; | |||
// private static readonly byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' }; | |||
// private static readonly byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; | |||
// private static readonly byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' }; | |||
private static readonly MethodInfo _writeTMethod = GetGenericWriteMethod(null); | |||
private static readonly MethodInfo _writeNullableTMethod = GetGenericWriteMethod(typeof(Nullable<>)); | |||
private static readonly MethodInfo _writeDictionaryTMethod = GetGenericWriteMethod(typeof(IDictionary<,>)); | |||
private static readonly MethodInfo _writeEnumerableTMethod = GetGenericWriteMethod(typeof(IEnumerable<>)); | |||
private static readonly DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |||
// private static readonly MethodInfo _writeTMethod = GetGenericWriteMethod(null); | |||
// private static readonly MethodInfo _writeNullableTMethod = GetGenericWriteMethod(typeof(Nullable<>)); | |||
// private static readonly MethodInfo _writeDictionaryTMethod = GetGenericWriteMethod(typeof(IDictionary<,>)); | |||
// private static readonly MethodInfo _writeEnumerableTMethod = GetGenericWriteMethod(typeof(IEnumerable<>)); | |||
// private static readonly DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |||
private readonly Stream _stream; | |||
private readonly byte[] _buffer; | |||
private readonly bool _leaveOpen; | |||
private readonly Encoding _encoding; | |||
// private readonly Stream _stream; | |||
// private readonly byte[] _buffer; | |||
// private readonly bool _leaveOpen; | |||
// private readonly Encoding _encoding; | |||
public virtual Stream BaseStream | |||
{ | |||
get | |||
{ | |||
Flush(); | |||
return _stream; | |||
} | |||
} | |||
// public virtual Stream BaseStream | |||
// { | |||
// get | |||
// { | |||
// Flush(); | |||
// return _stream; | |||
// } | |||
// } | |||
public ETFWriter(Stream stream, bool leaveOpen = false) | |||
{ | |||
if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
// public ETFWriter(Stream stream, bool leaveOpen = false) | |||
// { | |||
// if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
_stream = stream; | |||
_leaveOpen = leaveOpen; | |||
_buffer = new byte[11]; | |||
_encoding = Encoding.UTF8; | |||
} | |||
// _stream = stream; | |||
// _leaveOpen = leaveOpen; | |||
// _buffer = new byte[11]; | |||
// _encoding = Encoding.UTF8; | |||
// } | |||
public void Write(bool value) | |||
{ | |||
if (value) | |||
_stream.Write(_trueBytes, 0, _trueBytes.Length); | |||
else | |||
_stream.Write(_falseBytes, 0, _falseBytes.Length); | |||
} | |||
public void Write(sbyte value) => Write((long)value); | |||
public void Write(byte value) => Write((ulong)value); | |||
public void Write(short value) => Write((long)value); | |||
public void Write(ushort value) => Write((ulong)value); | |||
public void Write(int value) => Write((long)value); | |||
public void Write(uint value) => Write((ulong)value); | |||
public void Write(long value) | |||
{ | |||
if (value >= byte.MinValue && value <= byte.MaxValue) | |||
{ | |||
_buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; | |||
_buffer[1] = (byte)value; | |||
_stream.Write(_buffer, 0, 2); | |||
} | |||
else if (value >= int.MinValue && value <= int.MaxValue) | |||
{ | |||
//TODO: Does this encode negatives correctly? | |||
_buffer[0] = (byte)ETFType.INTEGER_EXT; | |||
_buffer[1] = (byte)(value >> 24); | |||
_buffer[2] = (byte)(value >> 16); | |||
_buffer[3] = (byte)(value >> 8); | |||
_buffer[4] = (byte)value; | |||
_stream.Write(_buffer, 0, 5); | |||
} | |||
else | |||
{ | |||
_buffer[0] = (byte)ETFType.SMALL_BIG_EXT; | |||
if (value < 0) | |||
{ | |||
_buffer[2] = 1; //Is negative | |||
value = -value; | |||
} | |||
// public void Write(bool value) | |||
// { | |||
// if (value) | |||
// _stream.Write(_trueBytes, 0, _trueBytes.Length); | |||
// else | |||
// _stream.Write(_falseBytes, 0, _falseBytes.Length); | |||
// } | |||
// public void Write(sbyte value) => Write((long)value); | |||
// public void Write(byte value) => Write((ulong)value); | |||
// public void Write(short value) => Write((long)value); | |||
// public void Write(ushort value) => Write((ulong)value); | |||
// public void Write(int value) => Write((long)value); | |||
// public void Write(uint value) => Write((ulong)value); | |||
// public void Write(long value) | |||
// { | |||
// if (value >= byte.MinValue && value <= byte.MaxValue) | |||
// { | |||
// _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; | |||
// _buffer[1] = (byte)value; | |||
// _stream.Write(_buffer, 0, 2); | |||
// } | |||
// else if (value >= int.MinValue && value <= int.MaxValue) | |||
// { | |||
// //TODO: Does this encode negatives correctly? | |||
// _buffer[0] = (byte)ETFType.INTEGER_EXT; | |||
// _buffer[1] = (byte)(value >> 24); | |||
// _buffer[2] = (byte)(value >> 16); | |||
// _buffer[3] = (byte)(value >> 8); | |||
// _buffer[4] = (byte)value; | |||
// _stream.Write(_buffer, 0, 5); | |||
// } | |||
// else | |||
// { | |||
// _buffer[0] = (byte)ETFType.SMALL_BIG_EXT; | |||
// if (value < 0) | |||
// { | |||
// _buffer[2] = 1; //Is negative | |||
// value = -value; | |||
// } | |||
byte bytes = 0; | |||
while (value > 0) | |||
_buffer[3 + bytes++] = (byte)(value >>= 8); | |||
_buffer[1] = bytes; //Encoded bytes | |||
// byte bytes = 0; | |||
// while (value > 0) | |||
// _buffer[3 + bytes++] = (byte)(value >>= 8); | |||
// _buffer[1] = bytes; //Encoded bytes | |||
_stream.Write(_buffer, 0, 3 + bytes); | |||
} | |||
} | |||
public void Write(ulong value) | |||
{ | |||
if (value <= byte.MaxValue) | |||
{ | |||
_buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; | |||
_buffer[1] = (byte)value; | |||
_stream.Write(_buffer, 0, 2); | |||
} | |||
else if (value <= int.MaxValue) | |||
{ | |||
_buffer[0] = (byte)ETFType.INTEGER_EXT; | |||
_buffer[1] = (byte)(value >> 24); | |||
_buffer[2] = (byte)(value >> 16); | |||
_buffer[3] = (byte)(value >> 8); | |||
_buffer[4] = (byte)value; | |||
_stream.Write(_buffer, 0, 5); | |||
} | |||
else | |||
{ | |||
_buffer[0] = (byte)ETFType.SMALL_BIG_EXT; | |||
_buffer[2] = 0; //Always positive | |||
// _stream.Write(_buffer, 0, 3 + bytes); | |||
// } | |||
// } | |||
// public void Write(ulong value) | |||
// { | |||
// if (value <= byte.MaxValue) | |||
// { | |||
// _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; | |||
// _buffer[1] = (byte)value; | |||
// _stream.Write(_buffer, 0, 2); | |||
// } | |||
// else if (value <= int.MaxValue) | |||
// { | |||
// _buffer[0] = (byte)ETFType.INTEGER_EXT; | |||
// _buffer[1] = (byte)(value >> 24); | |||
// _buffer[2] = (byte)(value >> 16); | |||
// _buffer[3] = (byte)(value >> 8); | |||
// _buffer[4] = (byte)value; | |||
// _stream.Write(_buffer, 0, 5); | |||
// } | |||
// else | |||
// { | |||
// _buffer[0] = (byte)ETFType.SMALL_BIG_EXT; | |||
// _buffer[2] = 0; //Always positive | |||
byte bytes = 0; | |||
while (value > 0) | |||
_buffer[3 + bytes++] = (byte)(value >>= 8); | |||
_buffer[1] = bytes; //Encoded bytes | |||
// byte bytes = 0; | |||
// while (value > 0) | |||
// _buffer[3 + bytes++] = (byte)(value >>= 8); | |||
// _buffer[1] = bytes; //Encoded bytes | |||
_stream.Write(_buffer, 0, 3 + bytes); | |||
} | |||
} | |||
// _stream.Write(_buffer, 0, 3 + bytes); | |||
// } | |||
// } | |||
public void Write(float value) => Write((double)value); | |||
public void Write(double value) | |||
{ | |||
ulong value2 = *(ulong*)&value; | |||
_buffer[0] = (byte)ETFType.NEW_FLOAT_EXT; | |||
_buffer[1] = (byte)(value2 >> 56); | |||
_buffer[2] = (byte)(value2 >> 48); | |||
_buffer[3] = (byte)(value2 >> 40); | |||
_buffer[4] = (byte)(value2 >> 32); | |||
_buffer[5] = (byte)(value2 >> 24); | |||
_buffer[6] = (byte)(value2 >> 16); | |||
_buffer[7] = (byte)(value2 >> 8); | |||
_buffer[8] = (byte)value2; | |||
_stream.Write(_buffer, 0, 9); | |||
} | |||
// public void Write(float value) => Write((double)value); | |||
// public void Write(double value) | |||
// { | |||
// ulong value2 = *(ulong*)&value; | |||
// _buffer[0] = (byte)ETFType.NEW_FLOAT_EXT; | |||
// _buffer[1] = (byte)(value2 >> 56); | |||
// _buffer[2] = (byte)(value2 >> 48); | |||
// _buffer[3] = (byte)(value2 >> 40); | |||
// _buffer[4] = (byte)(value2 >> 32); | |||
// _buffer[5] = (byte)(value2 >> 24); | |||
// _buffer[6] = (byte)(value2 >> 16); | |||
// _buffer[7] = (byte)(value2 >> 8); | |||
// _buffer[8] = (byte)value2; | |||
// _stream.Write(_buffer, 0, 9); | |||
// } | |||
public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerSecond)); | |||
// public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerSecond)); | |||
public void Write(bool? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } | |||
public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } | |||
public void Write(short? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } | |||
public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } | |||
public void Write(int? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } | |||
public void Write(long? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
public void Write(double? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); } | |||
public void Write(DateTime? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
// public void Write(bool? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
// public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } | |||
// public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } | |||
// public void Write(short? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } | |||
// public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } | |||
// public void Write(int? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
// public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } | |||
// public void Write(long? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
// public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
// public void Write(double? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
// public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); } | |||
// public void Write(DateTime? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } | |||
public void Write(string value) | |||
{ | |||
if (value != null) | |||
{ | |||
var bytes = _encoding.GetBytes(value); | |||
int count = bytes.Length; | |||
_buffer[0] = (byte)ETFType.BINARY_EXT; | |||
_buffer[1] = (byte)(count >> 24); | |||
_buffer[2] = (byte)(count >> 16); | |||
_buffer[3] = (byte)(count >> 8); | |||
_buffer[4] = (byte)count; | |||
_stream.Write(_buffer, 0, 5); | |||
_stream.Write(bytes, 0, bytes.Length); | |||
} | |||
else | |||
WriteNil(); | |||
} | |||
public void Write(byte[] value) | |||
{ | |||
if (value != null) | |||
{ | |||
int count = value.Length; | |||
_buffer[0] = (byte)ETFType.BINARY_EXT; | |||
_buffer[1] = (byte)(count >> 24); | |||
_buffer[2] = (byte)(count >> 16); | |||
_buffer[3] = (byte)(count >> 8); | |||
_buffer[4] = (byte)count; | |||
_stream.Write(_buffer, 0, 5); | |||
_stream.Write(value, 0, value.Length); | |||
} | |||
else | |||
WriteNil(); | |||
} | |||
// public void Write(string value) | |||
// { | |||
// if (value != null) | |||
// { | |||
// var bytes = _encoding.GetBytes(value); | |||
// int count = bytes.Length; | |||
// _buffer[0] = (byte)ETFType.BINARY_EXT; | |||
// _buffer[1] = (byte)(count >> 24); | |||
// _buffer[2] = (byte)(count >> 16); | |||
// _buffer[3] = (byte)(count >> 8); | |||
// _buffer[4] = (byte)count; | |||
// _stream.Write(_buffer, 0, 5); | |||
// _stream.Write(bytes, 0, bytes.Length); | |||
// } | |||
// else | |||
// WriteNil(); | |||
// } | |||
// public void Write(byte[] value) | |||
// { | |||
// if (value != null) | |||
// { | |||
// int count = value.Length; | |||
// _buffer[0] = (byte)ETFType.BINARY_EXT; | |||
// _buffer[1] = (byte)(count >> 24); | |||
// _buffer[2] = (byte)(count >> 16); | |||
// _buffer[3] = (byte)(count >> 8); | |||
// _buffer[4] = (byte)count; | |||
// _stream.Write(_buffer, 0, 5); | |||
// _stream.Write(value, 0, value.Length); | |||
// } | |||
// else | |||
// WriteNil(); | |||
// } | |||
public void Write<T>(T obj) | |||
{ | |||
var type = typeof(T); | |||
var typeInfo = type.GetTypeInfo(); | |||
var action = _serializers.GetOrAdd(type, _ => CreateSerializer<T>(type, typeInfo, false)) as Action<ETFWriter, T>; | |||
action(this, obj); | |||
} | |||
public void Write<T>(T? obj) | |||
where T : struct | |||
{ | |||
if (obj != null) | |||
Write(obj.Value); | |||
else | |||
WriteNil(); | |||
} | |||
public void Write<T>(IEnumerable<T> obj) | |||
{ | |||
if (obj != null) | |||
{ | |||
var array = obj.ToArray(); | |||
int length = array.Length; | |||
_buffer[0] = (byte)ETFType.LIST_EXT; | |||
_buffer[1] = (byte)(length >> 24); | |||
_buffer[2] = (byte)(length >> 16); | |||
_buffer[3] = (byte)(length >> 8); | |||
_buffer[4] = (byte)length; | |||
_stream.Write(_buffer, 0, 5); | |||
// public void Write<T>(T obj) | |||
// { | |||
// var type = typeof(T); | |||
// var typeInfo = type.GetTypeInfo(); | |||
// var action = _serializers.GetOrAdd(type, _ => CreateSerializer<T>(type, typeInfo, false)) as Action<ETFWriter, T>; | |||
// action(this, obj); | |||
// } | |||
// public void Write<T>(T? obj) | |||
// where T : struct | |||
// { | |||
// if (obj != null) | |||
// Write(obj.Value); | |||
// else | |||
// WriteNil(); | |||
// } | |||
// public void Write<T>(IEnumerable<T> obj) | |||
// { | |||
// if (obj != null) | |||
// { | |||
// var array = obj.ToArray(); | |||
// int length = array.Length; | |||
// _buffer[0] = (byte)ETFType.LIST_EXT; | |||
// _buffer[1] = (byte)(length >> 24); | |||
// _buffer[2] = (byte)(length >> 16); | |||
// _buffer[3] = (byte)(length >> 8); | |||
// _buffer[4] = (byte)length; | |||
// _stream.Write(_buffer, 0, 5); | |||
for (int i = 0; i < array.Length; i++) | |||
Write(array[i]); | |||
// for (int i = 0; i < array.Length; i++) | |||
// Write(array[i]); | |||
_buffer[0] = (byte)ETFType.NIL_EXT; | |||
_stream.Write(_buffer, 0, 1); | |||
} | |||
else | |||
WriteNil(); | |||
} | |||
public void Write<TKey, TValue>(IDictionary<TKey, TValue> obj) | |||
{ | |||
if (obj != null) | |||
{ | |||
int length = obj.Count; | |||
_buffer[0] = (byte)ETFType.MAP_EXT; | |||
_buffer[1] = (byte)(length >> 24); | |||
_buffer[2] = (byte)(length >> 16); | |||
_buffer[3] = (byte)(length >> 8); | |||
_buffer[4] = (byte)length; | |||
_stream.Write(_buffer, 0, 5); | |||
// _buffer[0] = (byte)ETFType.NIL_EXT; | |||
// _stream.Write(_buffer, 0, 1); | |||
// } | |||
// else | |||
// WriteNil(); | |||
// } | |||
// public void Write<TKey, TValue>(IDictionary<TKey, TValue> obj) | |||
// { | |||
// if (obj != null) | |||
// { | |||
// int length = obj.Count; | |||
// _buffer[0] = (byte)ETFType.MAP_EXT; | |||
// _buffer[1] = (byte)(length >> 24); | |||
// _buffer[2] = (byte)(length >> 16); | |||
// _buffer[3] = (byte)(length >> 8); | |||
// _buffer[4] = (byte)length; | |||
// _stream.Write(_buffer, 0, 5); | |||
foreach (var pair in obj) | |||
{ | |||
Write(pair.Key); | |||
Write(pair.Value); | |||
} | |||
} | |||
else | |||
WriteNil(); | |||
} | |||
public void Write(object obj) | |||
{ | |||
if (obj != null) | |||
{ | |||
var type = obj.GetType(); | |||
var typeInfo = type.GetTypeInfo(); | |||
var action = _indirectSerializers.GetOrAdd(type, _ => CreateSerializer<object>(type, typeInfo, true)) as Action<ETFWriter, object>; | |||
action(this, obj); | |||
} | |||
else | |||
WriteNil(); | |||
} | |||
// foreach (var pair in obj) | |||
// { | |||
// Write(pair.Key); | |||
// Write(pair.Value); | |||
// } | |||
// } | |||
// else | |||
// WriteNil(); | |||
// } | |||
// public void Write(object obj) | |||
// { | |||
// if (obj != null) | |||
// { | |||
// var type = obj.GetType(); | |||
// var typeInfo = type.GetTypeInfo(); | |||
// var action = _indirectSerializers.GetOrAdd(type, _ => CreateSerializer<object>(type, typeInfo, true)) as Action<ETFWriter, object>; | |||
// action(this, obj); | |||
// } | |||
// else | |||
// WriteNil(); | |||
// } | |||
private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length); | |||
// private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length); | |||
public virtual void Flush() => _stream.Flush(); | |||
public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin); | |||
// public virtual void Flush() => _stream.Flush(); | |||
// public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin); | |||
#region Emit | |||
private static Action<ETFWriter, T> CreateSerializer<T>(Type type, TypeInfo typeInfo, bool isDirect) | |||
{ | |||
var method = new DynamicMethod(isDirect ? "SerializeETF" : "SerializeIndirectETF", | |||
null, new[] { typeof(ETFWriter), isDirect ? type : typeof(object) }, true); | |||
var generator = method.GetILGenerator(); | |||
// #region Emit | |||
// private static Action<ETFWriter, T> CreateSerializer<T>(Type type, TypeInfo typeInfo, bool isDirect) | |||
// { | |||
// var method = new DynamicMethod(isDirect ? "SerializeETF" : "SerializeIndirectETF", | |||
// null, new[] { typeof(ETFWriter), isDirect ? type : typeof(object) }, true); | |||
// var generator = method.GetILGenerator(); | |||
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value | |||
if (!isDirect) | |||
{ | |||
if (typeInfo.IsValueType) //Unbox value types | |||
generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), real_value | |||
else //Cast reference types | |||
generator.Emit(OpCodes.Castclass, type); //ETFWriter(this), real_value | |||
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); //Call generic version | |||
} | |||
else | |||
EmitWriteValue(generator, type, typeInfo, true); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
// generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value | |||
// if (!isDirect) | |||
// { | |||
// if (typeInfo.IsValueType) //Unbox value types | |||
// generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), real_value | |||
// else //Cast reference types | |||
// generator.Emit(OpCodes.Castclass, type); //ETFWriter(this), real_value | |||
// generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); //Call generic version | |||
// } | |||
// else | |||
// EmitWriteValue(generator, type, typeInfo, true); | |||
generator.Emit(OpCodes.Ret); | |||
return method.CreateDelegate(typeof(Action<ETFWriter, T>)) as Action<ETFWriter, T>; | |||
} | |||
private static void EmitWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) | |||
{ | |||
//Convert enum types to their base type | |||
if (typeInfo.IsEnum) | |||
{ | |||
type = Enum.GetUnderlyingType(type); | |||
typeInfo = type.GetTypeInfo(); | |||
} | |||
// generator.Emit(OpCodes.Ret); | |||
// return method.CreateDelegate(typeof(Action<ETFWriter, T>)) as Action<ETFWriter, T>; | |||
// } | |||
// private static void EmitWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) | |||
// { | |||
// //Convert enum types to their base type | |||
// if (typeInfo.IsEnum) | |||
// { | |||
// type = Enum.GetUnderlyingType(type); | |||
// typeInfo = type.GetTypeInfo(); | |||
// } | |||
//Primitives/Enums | |||
Type targetType = null; | |||
if (!typeInfo.IsEnum && IsType(type, typeof(long), typeof(ulong), typeof(double), typeof(bool), typeof(string), | |||
typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), | |||
typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), | |||
typeof(bool?), typeof(float?), typeof(double?), | |||
typeof(object), typeof(DateTime))) | |||
{ | |||
//No conversion needed | |||
targetType = type; | |||
} | |||
else if (IsType(type, typeof(sbyte), typeof(short), typeof(int))) | |||
{ | |||
//Convert to long | |||
generator.Emit(OpCodes.Conv_I8); | |||
targetType = typeof(long); | |||
} | |||
else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint))) | |||
{ | |||
//Convert to ulong | |||
generator.Emit(OpCodes.Conv_U8); | |||
targetType = typeof(ulong); | |||
} | |||
else if (IsType(type, typeof(float))) | |||
{ | |||
//Convert to double | |||
generator.Emit(OpCodes.Conv_R8); | |||
targetType = typeof(double); | |||
} | |||
if (targetType != null) | |||
generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null); | |||
// //Primitives/Enums | |||
// Type targetType = null; | |||
// if (!typeInfo.IsEnum && IsType(type, typeof(long), typeof(ulong), typeof(double), typeof(bool), typeof(string), | |||
// typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), | |||
// typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), | |||
// typeof(bool?), typeof(float?), typeof(double?), | |||
// typeof(object), typeof(DateTime))) | |||
// { | |||
// //No conversion needed | |||
// targetType = type; | |||
// } | |||
// else if (IsType(type, typeof(sbyte), typeof(short), typeof(int))) | |||
// { | |||
// //Convert to long | |||
// generator.Emit(OpCodes.Conv_I8); | |||
// targetType = typeof(long); | |||
// } | |||
// else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint))) | |||
// { | |||
// //Convert to ulong | |||
// generator.Emit(OpCodes.Conv_U8); | |||
// targetType = typeof(ulong); | |||
// } | |||
// else if (IsType(type, typeof(float))) | |||
// { | |||
// //Convert to double | |||
// generator.Emit(OpCodes.Conv_R8); | |||
// targetType = typeof(double); | |||
// } | |||
// if (targetType != null) | |||
// generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null); | |||
//Dictionaries | |||
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) | |||
{ | |||
generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
//Enumerable | |||
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) | |||
{ | |||
generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
//Nullable Structs | |||
else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && | |||
typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) | |||
{ | |||
generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
//Structs/Classes | |||
else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) | |||
{ | |||
if (isTop) | |||
{ | |||
typeInfo.ForEachField(f => | |||
{ | |||
string name; | |||
if (!f.IsPublic || !IsETFProperty(f, out name)) return; | |||
// //Dictionaries | |||
// else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) | |||
// { | |||
// generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// //Enumerable | |||
// else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces | |||
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) | |||
// { | |||
// generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// //Nullable Structs | |||
// else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && | |||
// typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) | |||
// { | |||
// generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// //Structs/Classes | |||
// else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) | |||
// { | |||
// if (isTop) | |||
// { | |||
// typeInfo.ForEachField(f => | |||
// { | |||
// string name; | |||
// if (!f.IsPublic || !IsETFProperty(f, out name)) return; | |||
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name | |||
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj | |||
generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue | |||
EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); | |||
}); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
// generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name | |||
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
// generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj | |||
// generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue | |||
// EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); | |||
// }); | |||
typeInfo.ForEachProperty(p => | |||
{ | |||
string name; | |||
if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; | |||
// typeInfo.ForEachProperty(p => | |||
// { | |||
// string name; | |||
// if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; | |||
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name | |||
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj | |||
generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue | |||
EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); | |||
}); | |||
} | |||
else | |||
{ | |||
//While we could drill deeper and make a large serializer that also serializes all subclasses, | |||
//it's more efficient to serialize on a per-type basis via another Write<T> call. | |||
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
} | |||
} | |||
//Unsupported (decimal, char) | |||
else | |||
throw new InvalidOperationException($"Serializing {type.Name} is not supported."); | |||
} | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
// generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name | |||
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); | |||
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) | |||
// generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj | |||
// generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue | |||
// EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); | |||
// }); | |||
// } | |||
// else | |||
// { | |||
// //While we could drill deeper and make a large serializer that also serializes all subclasses, | |||
// //it's more efficient to serialize on a per-type basis via another Write<T> call. | |||
// generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); | |||
// } | |||
// } | |||
// //Unsupported (decimal, char) | |||
// else | |||
// throw new InvalidOperationException($"Serializing {type.Name} is not supported."); | |||
// } | |||
private static bool IsType(Type type, params Type[] types) | |||
{ | |||
for (int i = 0; i < types.Length; i++) | |||
{ | |||
if (type == types[i]) | |||
return true; | |||
} | |||
return false; | |||
} | |||
private static bool IsETFProperty(FieldInfo f, out string name) | |||
{ | |||
var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
if (attrib != null) | |||
{ | |||
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name; | |||
return true; | |||
} | |||
name = null; | |||
return false; | |||
} | |||
private static bool IsETFProperty(PropertyInfo p, out string name) | |||
{ | |||
var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
if (attrib != null) | |||
{ | |||
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name; | |||
return true; | |||
} | |||
name = null; | |||
return false; | |||
} | |||
// private static bool IsType(Type type, params Type[] types) | |||
// { | |||
// for (int i = 0; i < types.Length; i++) | |||
// { | |||
// if (type == types[i]) | |||
// return true; | |||
// } | |||
// return false; | |||
// } | |||
// private static bool IsETFProperty(FieldInfo f, out string name) | |||
// { | |||
// var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
// if (attrib != null) | |||
// { | |||
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name; | |||
// return true; | |||
// } | |||
// name = null; | |||
// return false; | |||
// } | |||
// private static bool IsETFProperty(PropertyInfo p, out string name) | |||
// { | |||
// var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); | |||
// if (attrib != null) | |||
// { | |||
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name; | |||
// return true; | |||
// } | |||
// name = null; | |||
// return false; | |||
// } | |||
private static MethodInfo GetWriteMethod(Type paramType) | |||
{ | |||
return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write)) | |||
.Where(x => x.GetParameters()[0].ParameterType == paramType) | |||
.Single(); | |||
} | |||
private static MethodInfo GetGenericWriteMethod(Type genericType) | |||
{ | |||
if (genericType == null) | |||
{ | |||
return typeof(ETFWriter).GetTypeInfo() | |||
.GetDeclaredMethods(nameof(Write)) | |||
.Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0]) | |||
.Single(); | |||
} | |||
else | |||
{ | |||
return typeof(ETFWriter).GetTypeInfo() | |||
.GetDeclaredMethods(nameof(Write)) | |||
.Where(x => | |||
{ | |||
if (!x.IsGenericMethodDefinition) return false; | |||
var p = x.GetParameters()[0].ParameterType.GetTypeInfo(); | |||
return p.IsGenericType && p.GetGenericTypeDefinition() == genericType; | |||
}) | |||
.Single(); | |||
} | |||
} | |||
#endregion | |||
// private static MethodInfo GetWriteMethod(Type paramType) | |||
// { | |||
// return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write)) | |||
// .Where(x => x.GetParameters()[0].ParameterType == paramType) | |||
// .Single(); | |||
// } | |||
// private static MethodInfo GetGenericWriteMethod(Type genericType) | |||
// { | |||
// if (genericType == null) | |||
// { | |||
// return typeof(ETFWriter).GetTypeInfo() | |||
// .GetDeclaredMethods(nameof(Write)) | |||
// .Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0]) | |||
// .Single(); | |||
// } | |||
// else | |||
// { | |||
// return typeof(ETFWriter).GetTypeInfo() | |||
// .GetDeclaredMethods(nameof(Write)) | |||
// .Where(x => | |||
// { | |||
// if (!x.IsGenericMethodDefinition) return false; | |||
// var p = x.GetParameters()[0].ParameterType.GetTypeInfo(); | |||
// return p.IsGenericType && p.GetGenericTypeDefinition() == genericType; | |||
// }) | |||
// .Single(); | |||
// } | |||
// } | |||
// #endregion | |||
#region IDisposable | |||
private bool _isDisposed = false; | |||
// #region IDisposable | |||
// private bool _isDisposed = false; | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (!_isDisposed) | |||
{ | |||
if (disposing) | |||
{ | |||
if (_leaveOpen) | |||
_stream.Flush(); | |||
else | |||
_stream.Dispose(); | |||
} | |||
_isDisposed = true; | |||
} | |||
} | |||
// protected virtual void Dispose(bool disposing) | |||
// { | |||
// if (!_isDisposed) | |||
// { | |||
// if (disposing) | |||
// { | |||
// if (_leaveOpen) | |||
// _stream.Flush(); | |||
// else | |||
// _stream.Dispose(); | |||
// } | |||
// _isDisposed = true; | |||
// } | |||
// } | |||
public void Dispose() => Dispose(true); | |||
#endregion | |||
} | |||
} | |||
// public void Dispose() => Dispose(true); | |||
// #endregion | |||
// } | |||
//} |
@@ -0,0 +1,8 @@ | |||
namespace Discord | |||
{ | |||
public enum GameType : int | |||
{ | |||
Default = 0, // "NotStreaming", pretty much | |||
Twitch | |||
} | |||
} |
@@ -6,7 +6,7 @@ | |||
CreateInstantInvite = 0, | |||
KickMembers = 1, | |||
BanMembers = 2, | |||
ManageRolesOrPermissions = 3, | |||
Administrator = 3, | |||
ManageChannel = 4, | |||
ManageServer = 5, | |||
@@ -26,6 +26,10 @@ | |||
MuteMembers = 22, | |||
DeafenMembers = 23, | |||
MoveMembers = 24, | |||
UseVoiceActivation = 25 | |||
UseVoiceActivation = 25, | |||
ChangeNickname = 26, | |||
ManageNicknames = 27, | |||
ManageRolesOrPermissions = 28, | |||
} | |||
} |
@@ -2,6 +2,6 @@ | |||
{ | |||
public enum Relative | |||
{ | |||
Before, After | |||
Before, After, Around | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
namespace Discord | |||
{ | |||
public enum TokenType | |||
{ | |||
User, | |||
Bot, | |||
} | |||
} |
@@ -8,6 +8,10 @@ | |||
public static UserStatus Idle { get; } = new UserStatus("idle"); | |||
/// <summary> User is offline. </summary> | |||
public static UserStatus Offline { get; } = new UserStatus("offline"); | |||
/// <summary> User is busy. </summary> | |||
public static UserStatus DoNotDisturb { get; } = new UserStatus("dnd"); | |||
/// <summary> User is invisible. </summary> | |||
public static UserStatus Invisible { get; } = new UserStatus("invisible"); | |||
private UserStatus(string value) | |||
: base(value) { } | |||
@@ -24,6 +28,10 @@ | |||
return Idle; | |||
case "offline": | |||
return Offline; | |||
case "dnd": | |||
return DoNotDisturb; | |||
case "invisible": | |||
return Invisible; | |||
default: | |||
return new UserStatus(value); | |||
} | |||
@@ -10,24 +10,19 @@ namespace Discord | |||
{ | |||
internal static class InternalExtensions | |||
{ | |||
internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture; | |||
public static ulong ToId(this string value) | |||
=> ulong.Parse(value, NumberStyles.None, _format); | |||
public static ulong? ToNullableId(this string value) | |||
=> value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); | |||
public static bool TryToId(this string value, out ulong result) | |||
=> ulong.TryParse(value, NumberStyles.None, _format, out result); | |||
internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture; | |||
public static ulong ToId(this string value) => ulong.Parse(value, NumberStyles.None, _format); | |||
public static ulong? ToNullableId(this string value) => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); | |||
public static bool TryToId(this string value, out ulong result) => ulong.TryParse(value, NumberStyles.None, _format, out result); | |||
public static string ToIdString(this ulong value) => value.ToString(_format); | |||
public static string ToIdString(this ulong? value) => value?.ToString(_format); | |||
public static string ToIdString(this ulong value) | |||
=> value.ToString(_format); | |||
public static string ToIdString(this ulong? value) | |||
=> value?.ToString(_format); | |||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
public static bool HasBit(this uint rawValue, byte bit) => ((rawValue >> bit) & 1U) == 1; | |||
public static bool TryGetOrAdd<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> d, | |||
public static bool TryGetOrAdd<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> d, | |||
TKey key, Func<TKey, TValue> factory, out TValue result) | |||
{ | |||
bool created = false; | |||
@@ -103,6 +98,16 @@ namespace Discord | |||
if (!exactMatch) | |||
{ | |||
if (name.Length >= 3 && name[0] == '<' && name[1] == '@' && name[2] == '!' && name[name.Length - 1] == '>') //Search by nickname'd mention | |||
{ | |||
ulong id; | |||
if (name.Substring(3, name.Length - 4).TryToId(out id)) | |||
{ | |||
var user = users.Where(x => x.Id == id).FirstOrDefault(); | |||
if (user != null) | |||
query = query.Concat(new User[] { user }); | |||
} | |||
} | |||
if (name.Length >= 2 && name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Search by raw mention | |||
{ | |||
ulong id; | |||
@@ -28,11 +28,6 @@ namespace Discord.Legacy | |||
{ | |||
client.ExecuteAndWait(asyncAction); | |||
} | |||
[Obsolete("Use DiscordClient.Wait")] | |||
public static void Run(this DiscordClient client) | |||
{ | |||
client.Wait(); | |||
} | |||
[Obsolete("Use Server.FindChannels")] | |||
public static IEnumerable<Channel> FindChannels(this DiscordClient client, Server server, string name, ChannelType type = null, bool exactMatch = false) | |||
@@ -7,36 +7,36 @@ namespace Discord.Logging | |||
LogSeverity Level { get; } | |||
void Log(LogSeverity severity, string message, Exception exception = null); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
void Log(LogSeverity severity, FormattableString message, Exception exception = null); | |||
#endif | |||
void Error(string message, Exception exception = null); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
void Error(FormattableString message, Exception exception = null); | |||
#endif | |||
void Error(Exception exception); | |||
void Warning(string message, Exception exception = null); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
void Warning(FormattableString message, Exception exception = null); | |||
#endif | |||
void Warning(Exception exception); | |||
void Info(string message, Exception exception = null); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
void Info(FormattableString message, Exception exception = null); | |||
#endif | |||
void Info(Exception exception); | |||
void Verbose(string message, Exception exception = null); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
void Verbose(FormattableString message, Exception exception = null); | |||
#endif | |||
void Verbose(Exception exception); | |||
void Debug(string message, Exception exception = null); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
void Debug(FormattableString message, Exception exception = null); | |||
#endif | |||
void Debug(Exception exception); | |||
@@ -25,7 +25,7 @@ namespace Discord.Logging | |||
} | |||
} | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
public void Log(LogSeverity severity, string source, FormattableString message, Exception exception = null) | |||
{ | |||
if (severity <= Level) | |||
@@ -57,7 +57,7 @@ namespace Discord.Logging | |||
public void Debug(string source, Exception ex) | |||
=> Log(LogSeverity.Debug, source, (string)null, ex); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
public void Error(string source, FormattableString message, Exception ex = null) | |||
=> Log(LogSeverity.Error, source, message, ex); | |||
public void Warning(string source, FormattableString message, Exception ex = null) | |||
@@ -38,7 +38,7 @@ namespace Discord.Logging | |||
public void Debug(Exception exception) | |||
=> _manager.Debug(Name, exception); | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
public void Log(LogSeverity severity, FormattableString message, Exception exception = null) | |||
=> _manager.Log(severity, Name, message, exception); | |||
public void Error(FormattableString message, Exception exception = null) | |||
@@ -74,7 +74,7 @@ namespace Discord | |||
public IEnumerable<Message> Messages => _messages?.Values ?? Enumerable.Empty<Message>(); | |||
/// <summary> Gets a collection of all custom permissions used for this channel. </summary> | |||
public IEnumerable<PermissionOverwrite> PermissionOverwrites => _permissionOverwrites.Select(x => x.Value); | |||
/// <summary> Gets a collection of all users with read access to this channel. </summary> | |||
public IEnumerable<User> Users | |||
{ | |||
@@ -131,7 +131,7 @@ namespace Discord | |||
if (client.Config.MessageCacheSize > 0) | |||
_messages = new ConcurrentDictionary<ulong, Message>(2, (int)(client.Config.MessageCacheSize * 1.05)); | |||
} | |||
internal void Update(APIChannel model) | |||
{ | |||
if (!IsPrivate && model.Name != null) | |||
@@ -199,7 +199,21 @@ namespace Discord | |||
await Server.ReorderChannels(channels.Skip(minPos), after).ConfigureAwait(false); | |||
} | |||
} | |||
public async Task DeleteMessages(Message[] messages) => await DeleteMessages(messages.Select(m => m.Id).ToArray()); | |||
public async Task DeleteMessages(ulong[] messageIds) | |||
{ | |||
if (messageIds.Count() > 100) | |||
throw new ArgumentOutOfRangeException("messageIds", | |||
"You must provide no more than 100 Messages or Message Ids"); | |||
if (messageIds.Count() == 1) | |||
await Client.ClientAPI.Send(new DeleteMessageRequest(Id, messageIds.First())); | |||
else if (messageIds.Any()) | |||
await Client.ClientAPI.Send(new BulkMessageDelete(Id, messageIds)); | |||
} | |||
public async Task Delete() | |||
{ | |||
try { await Client.ClientAPI.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } | |||
@@ -278,18 +292,25 @@ namespace Discord | |||
return new Message(id, this, userId != null ? GetUserFast(userId.Value) : null); | |||
} | |||
public async Task<Message[]> DownloadMessages(int limit = 100, ulong? relativeMessageId = null, | |||
public async Task<Message[]> DownloadMessages(int limit = 100, ulong? relativeMessageId = null, | |||
Relative relativeDir = Relative.Before, bool useCache = true) | |||
{ | |||
if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | |||
if (limit == 0 || Type != ChannelType.Text) return new Message[0]; | |||
try | |||
{ | |||
string dir; | |||
switch (relativeDir) | |||
{ | |||
case Relative.Before: default: dir = "before"; break; | |||
case Relative.After: dir = "after"; break; | |||
case Relative.Around: dir = "around"; break; | |||
} | |||
var request = new GetMessagesRequest(Id) | |||
{ | |||
Limit = limit, | |||
RelativeDir = relativeMessageId.HasValue ? relativeDir == Relative.Before ? "before" : "after" : null, | |||
RelativeDir = relativeMessageId.HasValue ? dir : null, | |||
RelativeId = relativeMessageId ?? 0 | |||
}; | |||
var msgs = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||
@@ -331,12 +352,12 @@ namespace Discord | |||
if (text == null) throw new ArgumentNullException(nameof(text)); | |||
if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text)); | |||
if (text.Length > DiscordConfig.MaxMessageSize) | |||
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); | |||
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); | |||
return Task.FromResult(Client.MessageQueue.QueueSend(this, text, isTTS)); | |||
} | |||
public async Task<Message> SendFile(string filePath) | |||
{ | |||
{ | |||
using (var stream = File.OpenRead(filePath)) | |||
return await SendFile(System.IO.Path.GetFileName(filePath), stream).ConfigureAwait(false); | |||
} | |||
@@ -381,7 +402,7 @@ namespace Discord | |||
if (_users.TryGetValue(user.Id, out member)) | |||
{ | |||
var perms = member.Permissions; | |||
if (UpdatePermissions(member.User, ref perms)) | |||
if (UpdatePermissions(member.User, ref perms)) | |||
_users[user.Id] = new Member(member.User, perms); | |||
} | |||
} | |||
@@ -398,8 +419,8 @@ namespace Discord | |||
//Start with this user's server permissions | |||
newPermissions = server.GetPermissions(user).RawValue; | |||
if (IsPrivate || user == Server.Owner) | |||
newPermissions = mask; //Owners always have all permissions | |||
if (IsPrivate || user == Server.Owner || newPermissions.HasBit((byte)PermissionBits.Administrator)) | |||
newPermissions = mask; //Owners and Administrators always have all permissions | |||
else | |||
{ | |||
var channelOverwrites = PermissionOverwrites; | |||
@@ -414,9 +435,7 @@ namespace Discord | |||
foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id && x.Permissions.AllowValue != 0)) | |||
newPermissions |= allowUser.Permissions.AllowValue; | |||
if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions)) | |||
newPermissions = mask; //ManageRolesOrPermissions gives all permisions | |||
else if (Type == ChannelType.Text && !newPermissions.HasBit((byte)PermissionBits.ReadMessages)) | |||
if (Type == ChannelType.Text && !newPermissions.HasBit((byte)PermissionBits.ReadMessages)) | |||
newPermissions = 0; //No read permission on a text channel removes all other permissions | |||
else if (Type == ChannelType.Voice && !newPermissions.HasBit((byte)PermissionBits.Connect)) | |||
newPermissions = 0; //No connect permissions on a voice channel removes all other permissions | |||
@@ -451,7 +470,7 @@ namespace Discord | |||
return perms; | |||
} | |||
} | |||
public ChannelPermissionOverrides GetPermissionsRule(User user) | |||
{ | |||
if (user == null) throw new ArgumentNullException(nameof(user)); | |||
@@ -470,7 +489,7 @@ namespace Discord | |||
.Select(x => x.Permissions) | |||
.FirstOrDefault(); | |||
} | |||
public Task AddPermissionsRule(User user, ChannelPermissions allow, ChannelPermissions deny) | |||
{ | |||
if (user == null) throw new ArgumentNullException(nameof(user)); | |||
@@ -506,7 +525,7 @@ namespace Discord | |||
}; | |||
return Client.ClientAPI.Send(request); | |||
} | |||
public Task RemovePermissionsRule(User user) | |||
{ | |||
if (user == null) throw new ArgumentNullException(nameof(user)); | |||
@@ -0,0 +1,22 @@ | |||
namespace Discord | |||
{ | |||
public struct Game | |||
{ | |||
public string Name { get; } | |||
public string Url { get; } | |||
public GameType Type { get; } | |||
public Game(string name) | |||
{ | |||
Name = name; | |||
Url = null; | |||
Type = GameType.Default; | |||
} | |||
public Game(string name, GameType type, string url) | |||
{ | |||
Name = name; | |||
Url = url; | |||
Type = type; | |||
} | |||
} | |||
} |
@@ -23,17 +23,32 @@ namespace Discord | |||
{ | |||
private readonly static Action<Message, Message> _cloner = DynamicIL.CreateCopyMethod<Message>(); | |||
private static readonly Regex _userRegex = new Regex(@"<@[0-9]+>"); | |||
private static readonly Regex _channelRegex = new Regex(@"<#[0-9]+>"); | |||
private static readonly Regex _roleRegex = new Regex(@"@everyone"); | |||
private static readonly Regex _userRegex = new Regex(@"<@[0-9]+>", RegexOptions.Compiled); | |||
private static readonly Regex _userNicknameRegex = new Regex(@"<@![0-9]+>", RegexOptions.Compiled); | |||
private static readonly Regex _channelRegex = new Regex(@"<#[0-9]+>", RegexOptions.Compiled); | |||
private static readonly Regex _roleRegex = new Regex(@"<@&[0-9]+>", RegexOptions.Compiled); | |||
private static readonly Attachment[] _initialAttachments = new Attachment[0]; | |||
private static readonly Embed[] _initialEmbeds = new Embed[0]; | |||
internal static string CleanUserMentions(Channel channel, string text, List<User> users = null) | |||
{ | |||
ulong id; | |||
text = _userNicknameRegex.Replace(text, new MatchEvaluator(e => | |||
{ | |||
if (e.Value.Substring(3, e.Value.Length - 4).TryToId(out id)) | |||
{ | |||
var user = channel.GetUserFast(id); | |||
if (user != null) | |||
{ | |||
if (users != null) | |||
users.Add(user); | |||
return '@' + user.Nickname; | |||
} | |||
} | |||
return e.Value; //User not found or parse failed | |||
})); | |||
return _userRegex.Replace(text, new MatchEvaluator(e => | |||
{ | |||
ulong id; | |||
if (e.Value.Substring(2, e.Value.Length - 3).TryToId(out id)) | |||
{ | |||
var user = channel.GetUserFast(id); | |||
@@ -43,7 +58,7 @@ namespace Discord | |||
users.Add(user); | |||
return '@' + user.Name; | |||
} | |||
} | |||
} | |||
return e.Value; //User not found or parse failed | |||
})); | |||
} | |||
@@ -68,28 +83,36 @@ namespace Discord | |||
return e.Value; //Channel not found or parse failed | |||
})); | |||
} | |||
/*internal static string CleanRoleMentions(User user, Channel channel, string text, List<Role> roles = null) | |||
internal static string CleanRoleMentions(Channel channel, string text, List<Role> roles = null) | |||
{ | |||
var server = channel.Server; | |||
if (server == null) return text; | |||
return _roleRegex.Replace(text, new MatchEvaluator(e => | |||
return _roleRegex.Replace(text, new MatchEvaluator(e => | |||
{ | |||
if (roles != null && user.GetPermissions(channel).MentionEveryone) | |||
roles.Add(server.EveryoneRole); | |||
return e.Value; | |||
ulong id; | |||
if (e.Value.Substring(3, e.Value.Length - 4).TryToId(out id)) | |||
{ | |||
var role = server.GetRole(id); | |||
if (role != null) | |||
{ | |||
if (roles != null) | |||
roles.Add(role); | |||
return "@" + role.Name; | |||
} | |||
} | |||
return e.Value; //Role not found or parse failed | |||
})); | |||
}*/ | |||
} | |||
//TODO: Move this somewhere | |||
private static string Resolve(Channel channel, string text) | |||
{ | |||
if (text == null) throw new ArgumentNullException(nameof(text)); | |||
var client = channel.Client; | |||
text = CleanUserMentions(channel, text); | |||
text = CleanChannelMentions(channel, text); | |||
//text = CleanRoleMentions(Channel, text); | |||
text = CleanRoleMentions(channel, text); | |||
return text; | |||
} | |||
@@ -281,32 +304,28 @@ namespace Discord | |||
.Where(x => x != null) | |||
.ToArray(); | |||
} | |||
if (model.IsMentioningEveryone != null) | |||
{ | |||
if (model.IsMentioningEveryone.Value && User != null && User.GetPermissions(channel).MentionEveryone) | |||
MentionedRoles = new Role[] { Server.EveryoneRole }; | |||
else | |||
MentionedRoles = new Role[0]; | |||
} | |||
if (model.Content != null) | |||
{ | |||
string text = model.Content; | |||
RawText = text; | |||
//var mentionedUsers = new List<User>(); | |||
var mentionedChannels = new List<Channel>(); | |||
//var mentionedRoles = new List<Role>(); | |||
var mentionedRoles = new List<Role>(); | |||
text = CleanUserMentions(Channel, text/*, mentionedUsers*/); | |||
if (server != null) | |||
{ | |||
text = CleanChannelMentions(Channel, text, mentionedChannels); | |||
//text = CleanRoleMentions(_client, User, channel, text, mentionedRoles); | |||
text = CleanRoleMentions(Channel, text, mentionedRoles); | |||
if (model.IsMentioningEveryone != null && model.IsMentioningEveryone.Value | |||
&& User != null && User.GetPermissions(channel).MentionEveryone) | |||
mentionedRoles.Add(Server.EveryoneRole); | |||
} | |||
Text = text; | |||
//MentionedUsers = mentionedUsers; | |||
MentionedChannels = mentionedChannels; | |||
//MentionedRoles = mentionedRoles; | |||
MentionedRoles = mentionedRoles; | |||
} | |||
} | |||
@@ -6,7 +6,7 @@ namespace Discord | |||
public struct ServerPermissions | |||
{ | |||
public static ServerPermissions None { get; } = new ServerPermissions(); | |||
public static ServerPermissions All { get; } = new ServerPermissions(Convert.ToUInt32("00000011111100111111110000111111", 2)); | |||
public static ServerPermissions All { get; } = new ServerPermissions(Convert.ToUInt32("00011111111100111111110000111111", 2)); | |||
public uint RawValue { get; } | |||
@@ -16,8 +16,8 @@ namespace Discord | |||
public bool BanMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.BanMembers); | |||
/// <summary> If True, a user may kick users from the server. </summary> | |||
public bool KickMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.KickMembers); | |||
/// <summary> If True, a user may adjust roles. This also implictly grants all other permissions. </summary> | |||
public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); | |||
/// <summary> If True, a user has all permissions and cannot have them revoked. </summary> | |||
public bool Administrator => PermissionsHelper.GetValue(RawValue, PermissionBits.Administrator); | |||
/// <summary> If True, a user may create, delete and modify channels. </summary> | |||
public bool ManageChannels => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel); | |||
/// <summary> If True, a user may adjust server properties. </summary> | |||
@@ -53,30 +53,37 @@ namespace Discord | |||
/// <summary> If True, a user may use voice activation rather than push-to-talk. </summary> | |||
public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation); | |||
public ServerPermissions(bool? createInstantInvite = null, bool? manageRoles = null, | |||
bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, | |||
/// <summary> If True, a user may change their own nickname. </summary> | |||
public bool ChangeNickname => PermissionsHelper.GetValue(RawValue, PermissionBits.ChangeNickname); | |||
/// <summary> If True, a user may change the nickname of other users. </summary> | |||
public bool ManageNicknames => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageNicknames); | |||
/// <summary> If True, a user may adjust roles. </summary> | |||
public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); | |||
public ServerPermissions(bool? createInstantInvite = null, bool? administrator = null, | |||
bool? banMembers = null, bool? kickMembers = null, bool? manageChannel = null, bool? manageServer = null, | |||
bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, | |||
bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, | |||
bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, | |||
bool? moveMembers = null, bool? useVoiceActivation = null) | |||
: this(new ServerPermissions(), createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageServer, readMessages, | |||
bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null) | |||
: this(new ServerPermissions(), createInstantInvite, administrator, banMembers, kickMembers, manageChannel, manageServer, readMessages, | |||
sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, | |||
moveMembers, useVoiceActivation) | |||
moveMembers, useVoiceActivation, manageRoles) | |||
{ | |||
} | |||
public ServerPermissions(ServerPermissions basePerms, bool? createInstantInvite = null, bool? manageRoles = null, | |||
bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, | |||
public ServerPermissions(ServerPermissions basePerms, bool? createInstantInvite = null, bool? administrator = null, | |||
bool? banMembers = null, bool? kickMembers = null, bool? manageChannel = null, bool? manageServer = null, | |||
bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, | |||
bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, | |||
bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, | |||
bool? moveMembers = null, bool? useVoiceActivation = null) | |||
bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null) | |||
{ | |||
uint value = basePerms.RawValue; | |||
PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite); | |||
PermissionsHelper.SetValue(ref value, administrator, PermissionBits.Administrator); | |||
PermissionsHelper.SetValue(ref value, banMembers, PermissionBits.BanMembers); | |||
PermissionsHelper.SetValue(ref value, kickMembers, PermissionBits.KickMembers); | |||
PermissionsHelper.SetValue(ref value, manageRoles, PermissionBits.ManageRolesOrPermissions); | |||
PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel); | |||
PermissionsHelper.SetValue(ref value, manageServer, PermissionBits.ManageServer); | |||
PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages); | |||
@@ -93,6 +100,9 @@ namespace Discord | |||
PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers); | |||
PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers); | |||
PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation); | |||
PermissionsHelper.SetValue(ref value, changeNickname, PermissionBits.ChangeNickname); | |||
PermissionsHelper.SetValue(ref value, manageNicknames, PermissionBits.ManageNicknames); | |||
PermissionsHelper.SetValue(ref value, manageRoles, PermissionBits.ManageRolesOrPermissions); | |||
RawValue = value; | |||
} | |||
@@ -102,9 +112,9 @@ namespace Discord | |||
public struct ChannelPermissions | |||
{ | |||
public static ChannelPermissions None { get; } = new ChannelPermissions(); | |||
public static ChannelPermissions TextOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000111111110000011001", 2)); | |||
public static ChannelPermissions TextOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00010000000000111111110000011001", 2)); | |||
public static ChannelPermissions PrivateOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000011100110000000000", 2)); | |||
public static ChannelPermissions VoiceOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000011111100000000000000011001", 2)); | |||
public static ChannelPermissions VoiceOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00010011111100000000000000011001", 2)); | |||
public static ChannelPermissions All(Channel channel) => All(channel.Type, channel.IsPrivate); | |||
public static ChannelPermissions All(ChannelType channelType, bool isPrivate) | |||
{ | |||
@@ -118,8 +128,6 @@ namespace Discord | |||
/// <summary> If True, a user may create invites. </summary> | |||
public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBits.CreateInstantInvite); | |||
/// <summary> If True, a user may adjust permissions. This also implictly grants all other permissions. </summary> | |||
public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); | |||
/// <summary> If True, a user may create, delete and modify this channel. </summary> | |||
public bool ManageChannel => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel); | |||
@@ -153,25 +161,28 @@ namespace Discord | |||
/// <summary> If True, a user may use voice activation rather than push-to-talk. </summary> | |||
public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation); | |||
public ChannelPermissions(bool? createInstantInvite = null, bool? managePermissions = null, | |||
/// <summary> If True, a user may adjust permissions. </summary> | |||
public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); | |||
public ChannelPermissions(bool? createInstantInvite = null, | |||
bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, | |||
bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, | |||
bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, | |||
bool? moveMembers = null, bool? useVoiceActivation = null) | |||
: this(new ChannelPermissions(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, | |||
manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) | |||
bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) | |||
: this(new ChannelPermissions(), createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, | |||
manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, | |||
moveMembers, useVoiceActivation, managePermissions) | |||
{ | |||
} | |||
public ChannelPermissions(ChannelPermissions basePerms, bool? createInstantInvite = null, bool? managePermissions = null, | |||
public ChannelPermissions(ChannelPermissions basePerms, bool? createInstantInvite = null, | |||
bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, | |||
bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, | |||
bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, | |||
bool? moveMembers = null, bool? useVoiceActivation = null) | |||
bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) | |||
{ | |||
uint value = basePerms.RawValue; | |||
PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite); | |||
PermissionsHelper.SetValue(ref value, managePermissions, PermissionBits.ManageRolesOrPermissions); | |||
PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel); | |||
PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages); | |||
PermissionsHelper.SetValue(ref value, sendMessages, PermissionBits.SendMessages); | |||
@@ -187,6 +198,7 @@ namespace Discord | |||
PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers); | |||
PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers); | |||
PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation); | |||
PermissionsHelper.SetValue(ref value, managePermissions, PermissionBits.ManageRolesOrPermissions); | |||
RawValue = value; | |||
} | |||
@@ -202,8 +214,6 @@ namespace Discord | |||
/// <summary> If True, a user may create invites. </summary> | |||
public PermValue CreateInstantInvite => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.CreateInstantInvite); | |||
/// <summary> If True, a user may adjust permissions. This also implictly grants all other permissions. </summary> | |||
public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageRolesOrPermissions); | |||
/// <summary> If True, a user may create, delete and modify this channel. </summary> | |||
public PermValue ManageChannel => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageChannel); | |||
/// <summary> If True, a user may join channels. </summary> | |||
@@ -223,8 +233,8 @@ namespace Discord | |||
/// <summary> If True, a user may mention @everyone. </summary> | |||
public PermValue MentionEveryone => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MentionEveryone); | |||
/// <summary> If True, a user may connect to a voice channel. </summary> | |||
public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Connect); | |||
/// <summary> If True, a user may connect to a voice channel. </summary> | |||
public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Connect); | |||
/// <summary> If True, a user may speak in a voice channel. </summary> | |||
public PermValue Speak => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Speak); | |||
/// <summary> If True, a user may mute users. </summary> | |||
@@ -236,25 +246,30 @@ namespace Discord | |||
/// <summary> If True, a user may use voice activation rather than push-to-talk. </summary> | |||
public PermValue UseVoiceActivation => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.UseVoiceActivation); | |||
public ChannelPermissionOverrides(PermValue? createInstantInvite = null, PermValue? managePermissions = null, | |||
/// <summary> If True, a user may adjust permissions. </summary> | |||
public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageRolesOrPermissions); | |||
public ChannelPermissionOverrides(PermValue? createInstantInvite = null, | |||
PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, | |||
PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, | |||
PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, | |||
PermValue? moveMembers = null, PermValue? useVoiceActivation = null) | |||
: this(new ChannelPermissionOverrides(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, | |||
manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) | |||
PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? changeNickname = null, PermValue? manageNicknames = null, | |||
PermValue? managePermissions = null) | |||
: this(new ChannelPermissionOverrides(), createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, | |||
manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, | |||
moveMembers, useVoiceActivation, managePermissions) | |||
{ | |||
} | |||
public ChannelPermissionOverrides(ChannelPermissionOverrides basePerms, PermValue? createInstantInvite = null, PermValue? managePermissions = null, | |||
public ChannelPermissionOverrides(ChannelPermissionOverrides basePerms, PermValue? createInstantInvite = null, | |||
PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, | |||
PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, | |||
PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, | |||
PermValue? moveMembers = null, PermValue? useVoiceActivation = null) | |||
PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? changeNickname = null, PermValue? manageNicknames = null, | |||
PermValue? managePermissions = null) | |||
{ | |||
uint allow = basePerms.AllowValue, deny = basePerms.DenyValue; | |||
PermissionsHelper.SetValue(ref allow, ref deny, createInstantInvite, PermissionBits.CreateInstantInvite); | |||
PermissionsHelper.SetValue(ref allow, ref deny, managePermissions, PermissionBits.ManageRolesOrPermissions); | |||
PermissionsHelper.SetValue(ref allow, ref deny, manageChannel, PermissionBits.ManageChannel); | |||
PermissionsHelper.SetValue(ref allow, ref deny, readMessages, PermissionBits.ReadMessages); | |||
PermissionsHelper.SetValue(ref allow, ref deny, sendMessages, PermissionBits.SendMessages); | |||
@@ -270,6 +285,9 @@ namespace Discord | |||
PermissionsHelper.SetValue(ref allow, ref deny, deafenMembers, PermissionBits.DeafenMembers); | |||
PermissionsHelper.SetValue(ref allow, ref deny, moveMembers, PermissionBits.MoveMembers); | |||
PermissionsHelper.SetValue(ref allow, ref deny, useVoiceActivation, PermissionBits.UseVoiceActivation); | |||
PermissionsHelper.SetValue(ref allow, ref deny, changeNickname, PermissionBits.ChangeNickname); | |||
PermissionsHelper.SetValue(ref allow, ref deny, manageNicknames, PermissionBits.ManageNicknames); | |||
PermissionsHelper.SetValue(ref allow, ref deny, managePermissions, PermissionBits.ManageRolesOrPermissions); | |||
AllowValue = allow; | |||
DenyValue = deny; | |||
@@ -23,12 +23,14 @@ namespace Discord | |||
/// <summary> Gets an id uniquely identifying from others with the same name. </summary> | |||
public ushort Discriminator => Client.PrivateUser.Discriminator; | |||
/// <summary> Gets the name of the game this user is currently playing. </summary> | |||
public string CurrentGame => Client.PrivateUser.CurrentGame; | |||
public Game? CurrentGame => Client.PrivateUser.CurrentGame; | |||
/// <summary> Gets the current status for this user. </summary> | |||
public UserStatus Status => Client.PrivateUser.Status; | |||
/// <summary> Returns the string used to mention this user. </summary> | |||
public string Mention => $"<@{Id}>"; | |||
/// <summary> Returns the string used to mention this user by nickname. </summary> | |||
public string NicknameMention => $"<@!{Id}>"; | |||
/// <summary> Gets the email for this user. </summary> | |||
public string Email { get; private set; } | |||
/// <summary> Gets if the email for this user has been verified. </summary> | |||
@@ -9,7 +9,7 @@ using APIRole = Discord.API.Client.Role; | |||
namespace Discord | |||
{ | |||
public class Role : IMentionable | |||
public class Role : IMentionable | |||
{ | |||
private readonly static Action<Role, Role> _cloner = DynamicIL.CreateCopyMethod<Role>(); | |||
@@ -28,6 +28,8 @@ namespace Discord | |||
public int Position { get; private set; } | |||
/// <summary> Gets whether this role is managed by server (e.g. for Twitch integration) </summary> | |||
public bool IsManaged { get; private set; } | |||
/// <summary> Gets whether this role is mentionable by anyone. </summary> | |||
public bool IsMentionable { get; private set; } | |||
/// <summary> Gets the the permissions given to this role. </summary> | |||
public ServerPermissions Permissions { get; private set; } | |||
/// <summary> Gets the color of this role. </summary> | |||
@@ -41,7 +43,7 @@ namespace Discord | |||
public IEnumerable<User> Members => IsEveryone ? Server.Users : Server.Users.Where(x => x.HasRole(this)); | |||
/// <summary> Gets the string used to mention this role. </summary> | |||
public string Mention => IsEveryone ? "@everyone" : ""; | |||
public string Mention => IsEveryone ? "@everyone" : IsMentionable ? $"<@&{Id}>" : ""; | |||
internal Role(ulong id, Server server) | |||
{ | |||
@@ -60,6 +62,8 @@ namespace Discord | |||
IsHoisted = model.Hoist.Value; | |||
if (model.Managed != null) | |||
IsManaged = model.Managed.Value; | |||
if (model.Mentionable != null) | |||
IsMentionable = model.Mentionable.Value; | |||
if (model.Position != null && !IsEveryone) | |||
Position = model.Position.Value; | |||
if (model.Color != null) | |||
@@ -75,14 +79,15 @@ namespace Discord | |||
} | |||
} | |||
public async Task Edit(string name = null, ServerPermissions? permissions = null, Color color = null, bool? isHoisted = null, int? position = null) | |||
public async Task Edit(string name = null, ServerPermissions? permissions = null, Color color = null, bool? isHoisted = null, int? position = null, bool? isMentionable = null) | |||
{ | |||
var updateRequest = new UpdateRoleRequest(Server.Id, Id) | |||
{ | |||
Name = name ?? Name, | |||
Permissions = (permissions ?? Permissions).RawValue, | |||
Color = (color ?? Color).RawValue, | |||
IsHoisted = isHoisted ?? IsHoisted | |||
IsHoisted = isHoisted ?? IsHoisted, | |||
IsMentionable = isMentionable ?? IsMentionable | |||
}; | |||
var updateResponse = await Client.ClientAPI.Send(updateRequest).ConfigureAwait(false); | |||
@@ -70,10 +70,6 @@ namespace Discord | |||
public int AFKTimeout { get; private set; } | |||
/// <summary> Gets the date and time you joined this server. </summary> | |||
public DateTime JoinedAt { get; private set; } | |||
/// <summary> Gets the default channel for this server. </summary> | |||
public Channel DefaultChannel { get; private set; } | |||
/// <summary> Gets the the role representing all users in a server. </summary> | |||
public Role EveryoneRole { get; private set; } | |||
/// <summary> Gets all extra features added to this server. </summary> | |||
public IEnumerable<string> Features { get; private set; } | |||
/// <summary> Gets all custom emojis on this server. </summary> | |||
@@ -93,6 +89,10 @@ namespace Discord | |||
public string IconUrl => GetIconUrl(Id, IconId); | |||
/// <summary> Gets the URL to this servers's splash image. </summary> | |||
public string SplashUrl => GetSplashUrl(Id, SplashId); | |||
/// <summary> Gets the default channel for this server. </summary> | |||
public Channel DefaultChannel => GetChannel(Id); | |||
/// <summary> Gets the the role representing all users in a server. </summary> | |||
public Role EveryoneRole => GetRole(Id); | |||
/// <summary> Gets a collection of all channels in this server. </summary> | |||
public IEnumerable<Channel> AllChannels => _channels.Select(x => x.Value); | |||
@@ -119,7 +119,7 @@ namespace Discord | |||
Client = client; | |||
Id = id; | |||
} | |||
internal void Update(Guild model) | |||
{ | |||
if (model.Name != null) | |||
@@ -144,7 +144,6 @@ namespace Discord | |||
var role = AddRole(x.Id); | |||
role.Update(x, false); | |||
} | |||
EveryoneRole = _roles[Id]; | |||
} | |||
if (model.Emojis != null) //Needs Roles | |||
{ | |||
@@ -171,7 +170,6 @@ namespace Discord | |||
_channels = new ConcurrentDictionary<ulong, Channel>(2, (int)(model.Channels.Length * 1.05)); | |||
foreach (var subModel in model.Channels) | |||
AddChannel(subModel.Id, false).Update(subModel); | |||
DefaultChannel = _channels[Id]; | |||
} | |||
if (model.MemberCount != null) | |||
{ | |||
@@ -198,7 +196,7 @@ namespace Discord | |||
} | |||
} | |||
} | |||
/// <summary> Edits this server, changing only non-null attributes. </summary> | |||
public Task Edit(string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) | |||
{ | |||
@@ -366,9 +364,9 @@ namespace Discord | |||
if (name == null) throw new ArgumentNullException(nameof(name)); | |||
return _roles.Select(x => x.Value).Find(name, exactMatch); | |||
} | |||
/// <summary> Creates a new role. </summary> | |||
public async Task<Role> CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false) | |||
public async Task<Role> CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false, bool isMentionable = false) | |||
{ | |||
if (name == null) throw new ArgumentNullException(nameof(name)); | |||
@@ -382,7 +380,8 @@ namespace Discord | |||
Name = name, | |||
Permissions = (permissions ?? role.Permissions).RawValue, | |||
Color = (color ?? Color.Default).RawValue, | |||
IsHoisted = isHoisted | |||
IsHoisted = isHoisted, | |||
IsMentionable = isMentionable | |||
}; | |||
var editResponse = await Client.ClientAPI.Send(editRequest).ConfigureAwait(false); | |||
role.Update(editResponse, true); | |||
@@ -405,16 +404,16 @@ namespace Discord | |||
#region Permissions | |||
internal ServerPermissions GetPermissions(User user) | |||
{ | |||
Member member; | |||
if (_users.TryGetValue(user.Id, out member)) | |||
return member.Permissions; | |||
else | |||
return ServerPermissions.None; | |||
} | |||
internal void UpdatePermissions(User user) | |||
{ | |||
{ | |||
Member member; | |||
if (_users.TryGetValue(user.Id, out member)) | |||
return member.Permissions; | |||
else | |||
return ServerPermissions.None; | |||
} | |||
internal void UpdatePermissions(User user) | |||
{ | |||
Member member; | |||
if (_users.TryGetValue(user.Id, out member)) | |||
{ | |||
@@ -426,30 +425,30 @@ namespace Discord | |||
channel.Value.UpdatePermissions(user); | |||
} | |||
} | |||
} | |||
} | |||
private bool UpdatePermissions(User user, ref ServerPermissions permissions) | |||
{ | |||
uint newPermissions = 0; | |||
if (user.Id == _ownerId) | |||
newPermissions = ServerPermissions.All.RawValue; | |||
else | |||
{ | |||
foreach (var serverRole in user.Roles) | |||
newPermissions |= serverRole.Permissions.RawValue; | |||
} | |||
if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions)) | |||
newPermissions = ServerPermissions.All.RawValue; | |||
if (newPermissions != permissions.RawValue) | |||
{ | |||
permissions = new ServerPermissions(newPermissions); | |||
{ | |||
uint newPermissions = 0; | |||
if (user.Id == _ownerId) | |||
newPermissions = ServerPermissions.All.RawValue; | |||
else | |||
{ | |||
foreach (var serverRole in user.Roles) | |||
newPermissions |= serverRole.Permissions.RawValue; | |||
} | |||
if (newPermissions.HasBit((byte)PermissionBits.Administrator)) | |||
newPermissions = ServerPermissions.All.RawValue; | |||
if (newPermissions != permissions.RawValue) | |||
{ | |||
permissions = new ServerPermissions(newPermissions); | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
} | |||
#endregion | |||
#region Users | |||
@@ -540,5 +539,5 @@ namespace Discord | |||
private Server() { } //Used for cloning | |||
public override string ToString() => Name ?? Id.ToIdString(); | |||
} | |||
} | |||
} |
@@ -58,10 +58,14 @@ namespace Discord | |||
public string Name { get; private set; } | |||
/// <summary> Gets an id uniquely identifying from others with the same name. </summary> | |||
public ushort Discriminator { get; private set; } | |||
/// <summary> Gets a user's nickname in a server </summary> | |||
public string Nickname { get; internal set; } | |||
/// <summary> Gets the unique identifier for this user's current avatar. </summary> | |||
public string AvatarId { get; private set; } | |||
/// <summary> Gets the name of the game this user is currently playing. </summary> | |||
public string CurrentGame { get; internal set; } | |||
public Game? CurrentGame { get; internal set; } | |||
/// <summary> Determines whether this user is a Bot account. </summary> | |||
public bool IsBot { get; internal set; } | |||
/// <summary> Gets the current status for this user. </summary> | |||
public UserStatus Status { get; internal set; } | |||
/// <summary> Gets the datetime that this user joined this server. </summary> | |||
@@ -79,6 +83,8 @@ namespace Discord | |||
public Channel PrivateChannel => Client.GetPrivateChannel(Id); | |||
/// <summary> Returns the string used to mention this user. </summary> | |||
public string Mention => $"<@{Id}>"; | |||
/// <summary> Returns the string used to mention this user by nickname. </summary> | |||
public string NicknameMention => $"<@!{Id}>"; | |||
/// <summary> Returns true if this user has marked themselves as muted. </summary> | |||
public bool IsSelfMuted => (_voiceState & VoiceState.SelfMuted) != 0; | |||
/// <summary> Returns true if this user has marked themselves as deafened. </summary> | |||
@@ -160,6 +166,9 @@ namespace Discord | |||
Discriminator = model.Discriminator.Value; | |||
if (model.Avatar != null) | |||
AvatarId = model.Avatar; | |||
if (model.Bot != null) | |||
IsBot = model.Bot.Value; | |||
} | |||
internal void Update(APIMember model) | |||
{ | |||
@@ -170,6 +179,8 @@ namespace Discord | |||
JoinedAt = model.JoinedAt.Value; | |||
if (model.Roles != null) | |||
UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); | |||
if (model.Nick != "") | |||
Nickname = model.Nick; | |||
} | |||
internal void Update(ExtendedMember model) | |||
{ | |||
@@ -198,8 +209,11 @@ namespace Discord | |||
if (Status == UserStatus.Offline) | |||
_lastOnline = DateTime.UtcNow; | |||
} | |||
CurrentGame = model.Game?.Name; //Allows null | |||
if (model.Game != null) | |||
CurrentGame = new Game(model.Game.Name, model.Game.Type ?? GameType.Default, model.Game.Url); | |||
else | |||
CurrentGame = null; | |||
} | |||
internal void Update(MemberVoiceState model) | |||
{ | |||
@@ -238,7 +252,7 @@ namespace Discord | |||
LastActivityAt = activity ?? DateTime.UtcNow; | |||
} | |||
public Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null) | |||
public async Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null, string nickname = "") | |||
{ | |||
if (Server == null) throw new InvalidOperationException("Unable to edit users in a private channel"); | |||
@@ -249,14 +263,29 @@ namespace Discord | |||
.Distinct() | |||
.ToArray(); | |||
var request = new UpdateMemberRequest(Server.Id, Id) | |||
bool isCurrentUser = Id == Server.CurrentUser.Id; | |||
if (isCurrentUser && nickname != "") | |||
{ | |||
IsMuted = isMuted ?? IsServerMuted, | |||
IsDeafened = isDeafened ?? IsServerDeafened, | |||
VoiceChannelId = voiceChannel?.Id, | |||
RoleIds = roleIds | |||
}; | |||
return Client.ClientAPI.Send(request); | |||
var request = new UpdateOwnNick(Server.Id, nickname ?? ""); | |||
await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||
nickname = ""; | |||
} | |||
if (!isCurrentUser || isMuted != null || isDeafened != null || voiceChannel != null || roles != null) | |||
{ | |||
//Swap "" and null. Our libs meanings and the API's are flipped. | |||
if (nickname == null) nickname = ""; | |||
else if (nickname == "") nickname = null; | |||
var request = new UpdateMemberRequest(Server.Id, Id) | |||
{ | |||
IsMuted = isMuted ?? IsServerMuted, | |||
IsDeafened = isDeafened ?? IsServerDeafened, | |||
VoiceChannelId = voiceChannel?.Id, | |||
RoleIds = roleIds, | |||
Nickname = nickname | |||
}; | |||
await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||
} | |||
} | |||
public Task Kick() | |||
@@ -268,7 +297,16 @@ namespace Discord | |||
} | |||
#region Permissions | |||
public ServerPermissions ServerPermissions => Server.GetPermissions(this); | |||
public ServerPermissions ServerPermissions | |||
{ | |||
get | |||
{ | |||
if (Server == null) throw new InvalidOperationException("Unable to get server permissions from a private channel"); | |||
return Server.GetPermissions(this); | |||
} | |||
} | |||
public ChannelPermissions GetPermissions(Channel channel) | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
@@ -362,6 +400,6 @@ namespace Discord | |||
} | |||
private User() { } //Used for cloning | |||
public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); | |||
public override string ToString() => Name != null ? $"{Name}#{Discriminator.ToString("D4")}" : Id.ToIdString(); | |||
} | |||
} |
@@ -1,4 +1,4 @@ | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
using Discord.Logging; | |||
using System; | |||
using System.IO; | |||
@@ -37,8 +37,7 @@ namespace Discord.Net.Rest | |||
{ | |||
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, | |||
UseCookies = false, | |||
UseProxy = false, | |||
PreAuthenticate = false //We do auth ourselves | |||
UseProxy = false | |||
}); | |||
_client.DefaultRequestHeaders.Add("accept", "*/*"); | |||
_client.DefaultRequestHeaders.Add("accept-encoding", "gzip,deflate"); | |||
@@ -66,7 +65,7 @@ namespace Discord.Net.Rest | |||
using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path)) | |||
{ | |||
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | |||
content.Add(new StreamContent(File.OpenRead(path)), "file", filename); | |||
content.Add(new StreamContent(stream), "file", filename); | |||
request.Content = content; | |||
return await Send(request, cancelToken).ConfigureAwait(false); | |||
} | |||
@@ -1,27 +0,0 @@ | |||
using Discord.ETF; | |||
using System.IO; | |||
using System; | |||
using Discord.Logging; | |||
namespace Discord.Net.Rest | |||
{ | |||
public class ETFRestClient : RestClient | |||
{ | |||
private readonly ETFWriter _serializer; | |||
public ETFRestClient(DiscordConfig config, string baseUrl, ILogger logger = null) | |||
: base(config, baseUrl, logger) | |||
{ | |||
_serializer = new ETFWriter(new MemoryStream()); | |||
} | |||
protected override string Serialize<T>(T obj) | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
protected override T Deserialize<T>(string json) | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
} | |||
} |
@@ -36,7 +36,6 @@ namespace Discord.Net.Rest | |||
private readonly DiscordConfig _config; | |||
private readonly IRestEngine _engine; | |||
private readonly ETFWriter _serializer; | |||
private readonly ILogger _logger; | |||
private string _token; | |||
@@ -52,12 +51,15 @@ namespace Discord.Net.Rest | |||
} | |||
} | |||
public int ShardId => _config.ShardId; | |||
public int TotalShards => _config.TotalShards; | |||
protected RestClient(DiscordConfig config, string baseUrl, ILogger logger = null) | |||
{ | |||
_config = config; | |||
_logger = logger; | |||
#if !DOTNET5_4 | |||
#if !NETSTANDARD1_3 | |||
_engine = new RestSharpEngine(config, baseUrl, logger); | |||
#else | |||
_engine = new BuiltInEngine(config, baseUrl, logger); | |||
@@ -1,4 +1,4 @@ | |||
#if !DOTNET5_4 | |||
#if !NETSTANDARD1_3 | |||
using Discord.Logging; | |||
using Nito.AsyncEx; | |||
using RestSharp; | |||
@@ -1,4 +1,4 @@ | |||
#if DOTNET5_4 | |||
#if NETSTANDARD1_3 | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
@@ -51,8 +51,7 @@ namespace Discord.Net.WebSockets | |||
{ | |||
string ignored; | |||
while (_sendQueue.TryDequeue(out ignored)) { } | |||
var socket = _webSocket; | |||
_webSocket = null; | |||
return TaskHelper.CompletedTask; | |||
@@ -89,7 +88,7 @@ namespace Discord.Net.WebSockets | |||
if (result.MessageType == WebSocketMessageType.Close) | |||
throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); | |||
else | |||
stream.Write(buffer.Array, buffer.Offset, buffer.Count); | |||
stream.Write(buffer.Array, 0, result.Count); | |||
} | |||
while (result == null || !result.EndOfMessage); | |||
@@ -11,6 +11,7 @@ using System.IO; | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using APIGame = Discord.API.Client.Game; | |||
namespace Discord.Net.WebSockets | |||
{ | |||
@@ -43,12 +44,13 @@ namespace Discord.Net.WebSockets | |||
//Token = rest.Token; | |||
var gatewayResponse = await rest.Send(new GatewayRequest()).ConfigureAwait(false); | |||
Logger.Verbose($"Login successful, gateway: {gatewayResponse.Url}"); | |||
string url = $"{gatewayResponse.Url}/?encoding=json&v=4"; | |||
Logger.Verbose($"Login successful, gateway: {url}"); | |||
Host = gatewayResponse.Url; | |||
Host = url; | |||
await BeginConnect(parentCancelToken).ConfigureAwait(false); | |||
if (SessionId == null) | |||
SendIdentify(_rest.Token); | |||
SendIdentify(_rest.Token, _rest.ShardId, _rest.TotalShards); | |||
else | |||
SendResume(); | |||
} | |||
@@ -131,15 +133,10 @@ namespace Discord.Net.WebSockets | |||
} | |||
} | |||
break; | |||
case OpCodes.Redirect: | |||
case OpCodes.Reconnect: | |||
{ | |||
var payload = (msg.Payload as JToken).ToObject<RedirectEvent>(_serializer); | |||
if (payload.Url != null) | |||
{ | |||
Host = payload.Url; | |||
Logger.Info("Redirected to " + payload.Url); | |||
await Reconnect().ConfigureAwait(false); | |||
} | |||
var payload = (msg.Payload as JToken).ToObject<ReconnectEvent>(_serializer); | |||
await Reconnect().ConfigureAwait(false); | |||
} | |||
break; | |||
default: | |||
@@ -151,7 +148,7 @@ namespace Discord.Net.WebSockets | |||
} | |||
} | |||
public void SendIdentify(string token) | |||
public void SendIdentify(string token, int shardId = 0, int totalShards = 1) | |||
{ | |||
var props = new Dictionary<string, string> | |||
{ | |||
@@ -159,24 +156,27 @@ namespace Discord.Net.WebSockets | |||
}; | |||
var msg = new IdentifyCommand() | |||
{ | |||
Version = 3, | |||
Token = token, | |||
Properties = props, | |||
Properties = props, | |||
LargeThreshold = _config.LargeThreshold, | |||
UseCompression = true | |||
UseCompression = true, | |||
ShardingParams = token.StartsWith("Bot ") ? new int[] { shardId, totalShards } : null | |||
}; | |||
QueueMessage(msg); | |||
QueueMessage(msg); | |||
} | |||
public void SendResume() | |||
=> QueueMessage(new ResumeCommand { SessionId = SessionId, Sequence = _lastSequence }); | |||
public override void SendHeartbeat() | |||
=> QueueMessage(new HeartbeatCommand()); | |||
public void SendUpdateStatus(long? idleSince, string gameName) | |||
public void SendUpdateStatus(long? idleSince, Game? game, bool? afk, UserStatus status) | |||
=> QueueMessage(new UpdateStatusCommand | |||
{ | |||
IdleSince = idleSince, | |||
Game = gameName != null ? new UpdateStatusCommand.GameInfo { Name = gameName } : null | |||
Game = game != null ? new APIGame { Name = game.Value.Name, Type = game.Value.Type, Url = game.Value.Url } : null, | |||
Afk = afk, | |||
Status = status.Value | |||
}); | |||
public void SendUpdateVoice(ulong? serverId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened) | |||
=> QueueMessage(new UpdateVoiceCommand { GuildId = serverId, ChannelId = channelId, IsSelfMuted = isSelfMuted, IsSelfDeafened = isSelfDeafened }); | |||
@@ -1,4 +1,4 @@ | |||
#if !DOTNET5_4 | |||
#if !NETSTANDARD1_3 | |||
using SuperSocket.ClientEngine; | |||
using System; | |||
using System.Collections.Concurrent; | |||
@@ -10,36 +10,36 @@ using System.Threading.Tasks; | |||
namespace Discord.Net.WebSockets | |||
{ | |||
public abstract partial class WebSocket | |||
public abstract partial class WebSocket | |||
{ | |||
private readonly AsyncLock _lock; | |||
protected readonly IWebSocketEngine _engine; | |||
protected readonly DiscordConfig _config; | |||
protected readonly ManualResetEventSlim _connectedEvent; | |||
protected readonly DiscordConfig _config; | |||
protected readonly ManualResetEventSlim _connectedEvent; | |||
protected readonly TaskManager _taskManager; | |||
protected readonly JsonSerializer _serializer; | |||
protected CancellationTokenSource _cancelSource; | |||
protected CancellationToken _parentCancelToken; | |||
protected int _heartbeatInterval; | |||
private DateTime _lastHeartbeat; | |||
private DateTime _lastHeartbeat; | |||
/// <summary> Gets the logger used for this client. </summary> | |||
protected internal Logger Logger { get; } | |||
public CancellationToken CancelToken { get; private set; } | |||
public string Host { get; set; } | |||
public string Host { get; set; } | |||
/// <summary> Gets the current connection state of this client. </summary> | |||
public ConnectionState State { get; private set; } | |||
public event EventHandler Connected = delegate { }; | |||
private void OnConnected() | |||
=> Connected(this, EventArgs.Empty); | |||
private void OnConnected() | |||
=> Connected(this, EventArgs.Empty); | |||
public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { }; | |||
private void OnDisconnected(bool wasUnexpected, Exception error) | |||
private void OnDisconnected(bool wasUnexpected, Exception error) | |||
=> Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); | |||
public WebSocket(DiscordConfig config, JsonSerializer serializer, Logger logger) | |||
{ | |||
public WebSocket(DiscordConfig config, JsonSerializer serializer, Logger logger) | |||
{ | |||
_config = config; | |||
_serializer = serializer; | |||
Logger = logger; | |||
@@ -47,30 +47,30 @@ namespace Discord.Net.WebSockets | |||
_lock = new AsyncLock(); | |||
_taskManager = new TaskManager(Cleanup); | |||
CancelToken = new CancellationToken(true); | |||
_connectedEvent = new ManualResetEventSlim(false); | |||
_connectedEvent = new ManualResetEventSlim(false); | |||
#if !DOTNET5_4 | |||
_engine = new WS4NetEngine(config, _taskManager); | |||
#if !NETSTANDARD1_3 | |||
_engine = new WS4NetEngine(config, _taskManager); | |||
#else | |||
_engine = new BuiltInEngine(config); | |||
#endif | |||
_engine.BinaryMessage += (s, e) => | |||
{ | |||
using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) | |||
using (var decompressed = new MemoryStream()) | |||
{ | |||
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) | |||
zlib.CopyTo(decompressed); | |||
decompressed.Position = 0; | |||
using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) | |||
using (var decompressed = new MemoryStream()) | |||
{ | |||
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) | |||
zlib.CopyTo(decompressed); | |||
decompressed.Position = 0; | |||
using (var reader = new StreamReader(decompressed)) | |||
ProcessMessage(reader.ReadToEnd()).GetAwaiter().GetResult(); | |||
} | |||
ProcessMessage(reader.ReadToEnd()).GetAwaiter().GetResult(); | |||
} | |||
}; | |||
_engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); | |||
} | |||
_engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); | |||
} | |||
protected async Task BeginConnect(CancellationToken parentCancelToken) | |||
{ | |||
protected async Task BeginConnect(CancellationToken parentCancelToken) | |||
{ | |||
try | |||
{ | |||
using (await _lock.LockAsync().ConfigureAwait(false)) | |||
@@ -80,7 +80,7 @@ namespace Discord.Net.WebSockets | |||
await _taskManager.Stop().ConfigureAwait(false); | |||
_taskManager.ClearException(); | |||
State = ConnectionState.Connecting; | |||
_cancelSource = new CancellationTokenSource(); | |||
CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelSource.Token, parentCancelToken).Token; | |||
_lastHeartbeat = DateTime.UtcNow; | |||
@@ -88,39 +88,39 @@ namespace Discord.Net.WebSockets | |||
await _engine.Connect(Host, CancelToken).ConfigureAwait(false); | |||
await Run().ConfigureAwait(false); | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
} | |||
catch (Exception ex) | |||
{ | |||
//TODO: Should this be inside the lock? | |||
await _taskManager.SignalError(ex).ConfigureAwait(false); | |||
throw; | |||
} | |||
} | |||
protected async Task EndConnect() | |||
{ | |||
try | |||
} | |||
} | |||
protected async Task EndConnect() | |||
{ | |||
try | |||
{ | |||
State = ConnectionState.Connected; | |||
Logger.Info($"Connected"); | |||
OnConnected(); | |||
_connectedEvent.Set(); | |||
} | |||
catch (Exception ex) | |||
} | |||
catch (Exception ex) | |||
{ | |||
await _taskManager.SignalError(ex).ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
protected abstract Task Run(); | |||
protected virtual async Task Cleanup() | |||
{ | |||
protected abstract Task Run(); | |||
protected virtual async Task Cleanup() | |||
{ | |||
var oldState = State; | |||
State = ConnectionState.Disconnecting; | |||
await _engine.Disconnect().ConfigureAwait(false); | |||
_cancelSource = null; | |||
_connectedEvent.Reset(); | |||
_cancelSource = null; | |||
_connectedEvent.Reset(); | |||
if (oldState == ConnectionState.Connecting || oldState == ConnectionState.Connected) | |||
{ | |||
@@ -136,35 +136,35 @@ namespace Discord.Net.WebSockets | |||
State = ConnectionState.Disconnected; | |||
} | |||
protected virtual Task ProcessMessage(string json) | |||
{ | |||
return TaskHelper.CompletedTask; | |||
} | |||
protected void QueueMessage(IWebSocketMessage message) | |||
{ | |||
string json = JsonConvert.SerializeObject(new WebSocketMessage(message)); | |||
_engine.QueueMessage(json); | |||
} | |||
protected Task HeartbeatAsync(CancellationToken cancelToken) | |||
{ | |||
return Task.Run(async () => | |||
{ | |||
try | |||
{ | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
if (this.State == ConnectionState.Connected && _heartbeatInterval > 0) | |||
{ | |||
protected virtual Task ProcessMessage(string json) | |||
{ | |||
return TaskHelper.CompletedTask; | |||
} | |||
protected void QueueMessage(IWebSocketMessage message) | |||
{ | |||
string json = JsonConvert.SerializeObject(new WebSocketMessage(message)); | |||
_engine.QueueMessage(json); | |||
} | |||
protected Task HeartbeatAsync(CancellationToken cancelToken) | |||
{ | |||
return Task.Run(async () => | |||
{ | |||
try | |||
{ | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
if (this.State == ConnectionState.Connected && _heartbeatInterval > 0) | |||
{ | |||
SendHeartbeat(); | |||
await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); | |||
} | |||
else | |||
await Task.Delay(1000, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
catch (OperationCanceledException) { } | |||
}); | |||
await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); | |||
} | |||
else | |||
await Task.Delay(1000, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
catch (OperationCanceledException) { } | |||
}); | |||
} | |||
public abstract void SendHeartbeat(); | |||
@@ -184,5 +184,5 @@ namespace Discord.Net.WebSockets | |||
throw; | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -20,6 +20,7 @@ namespace Discord | |||
public bool StopOnCompletion { get; } | |||
public bool WasStopExpected { get; private set; } | |||
public CancellationToken CancelToken => _cancelSource.Token; | |||
public Exception Exception => _stopReason?.SourceException; | |||
@@ -1,65 +1,48 @@ | |||
{ | |||
"version": "0.9.0-rc3-3", | |||
"version": "0.9.6", | |||
"description": "An unofficial .Net API wrapper for the Discord client.", | |||
"authors": [ | |||
"RogueException" | |||
], | |||
"tags": [ | |||
"discord", | |||
"discordapp" | |||
], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
"authors": [ "RogueException" ], | |||
"packOptions": { | |||
"tags": [ | |||
"discord", | |||
"discordapp" | |||
], | |||
"projectUrl": "https://github.com/RogueException/Discord.Net", | |||
"licenseUrl": "http://opensource.org/licenses/MIT", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/RogueException/Discord.Net" | |||
} | |||
}, | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"compilationOptions": { | |||
"buildOptions": { | |||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], | |||
"allowUnsafe": true, | |||
"warningsAsErrors": true | |||
}, | |||
"configurations": { | |||
"TestResponses": { | |||
"compilationOptions": { | |||
"define": [ | |||
"DEBUG", | |||
"TRACE", | |||
"TEST_RESPONSES" | |||
] | |||
} | |||
} | |||
}, | |||
"dependencies": { | |||
"Newtonsoft.Json": "8.0.1", | |||
"Newtonsoft.Json": "8.0.3", | |||
"Nito.AsyncEx": "3.0.1" | |||
}, | |||
"frameworks": { | |||
"dotnet5.4": { | |||
"netstandard1.3": { | |||
"dependencies": { | |||
"System.Collections": "4.0.11-beta-23516", | |||
"System.Collections.Concurrent": "4.0.11-beta-23516", | |||
"System.Dynamic.Runtime": "4.0.11-beta-23516", | |||
"System.IO.FileSystem": "4.0.1-beta-23516", | |||
"System.IO.Compression": "4.1.0-beta-23516", | |||
"System.Linq": "4.0.1-beta-23516", | |||
"System.Net.Http": "4.0.1-beta-23516", | |||
"System.Net.NameResolution": "4.0.0-beta-23516", | |||
"System.Net.Sockets": "4.1.0-beta-23409", | |||
"System.Net.Requests": "4.0.11-beta-23516", | |||
"System.Net.WebSockets.Client": "4.0.0-beta-23516", | |||
"System.Reflection": "4.1.0-beta-23516", | |||
"System.Reflection.Emit.Lightweight": "4.0.1-beta-23516", | |||
"System.Runtime.InteropServices": "4.0.21-beta-23516", | |||
"System.Runtime.Serialization.Primitives": "4.1.0-beta-23516", | |||
"System.Security.Cryptography.Algorithms": "4.0.0-beta-23516", | |||
"System.Text.RegularExpressions": "4.0.11-beta-23516", | |||
"System.Threading": "4.0.11-beta-23516" | |||
} | |||
"NETStandard.Library": "1.6.0", | |||
"System.Net.Requests": "4.0.11", | |||
"System.Net.Websockets.Client": "4.0.0", | |||
"System.Reflection.Emit.Lightweight": "4.0.1", | |||
"System.Runtime.Serialization.Primitives": "4.1.1", | |||
"System.Security.Cryptography.Algorithms": "4.2.0", | |||
"System.Net.NameResolution": "4.0.0" | |||
}, | |||
"imports": [ | |||
"dotnet5.4", | |||
"dnxcore50", | |||
"portable-net45+win8" | |||
] | |||
}, | |||
"net45": { | |||
"frameworkAssemblies": { | |||
@@ -37,7 +37,7 @@ | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\DiscordBot\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> | |||
<HintPath>..\..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
<Reference Include="System" /> | |||
@@ -56,18 +56,17 @@ | |||
</Choose> | |||
<ItemGroup> | |||
<Compile Include="Tests.cs" /> | |||
<Compile Include="Settings.cs" /> | |||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Include="packages.config" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\..\src\Discord.Net.Net45\Discord.Net.csproj"> | |||
<Project>{8d71a857-879a-4a10-859e-5ff824ed6688}</Project> | |||
<Name>Discord.Net</Name> | |||
</ProjectReference> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Include="packages.config" /> | |||
</ItemGroup> | |||
<Choose> | |||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'"> | |||
<ItemGroup> | |||
@@ -1,32 +0,0 @@ | |||
using Newtonsoft.Json; | |||
using System.IO; | |||
namespace Discord.Tests | |||
{ | |||
internal class Settings | |||
{ | |||
private const string path = "../../config.json"; | |||
public static readonly Settings Instance; | |||
static Settings() | |||
{ | |||
if (!File.Exists(path)) | |||
throw new FileNotFoundException("config.json is missing, rename config.json.example and add credentials for three separate unused accounts for testing."); | |||
Instance = JsonConvert.DeserializeObject<Settings>(File.ReadAllText(path)); | |||
} | |||
public class Account | |||
{ | |||
[JsonProperty("email")] | |||
public string Email { get; set; } | |||
[JsonProperty("password")] | |||
public string Password { get; set; } | |||
} | |||
[JsonProperty("user1")] | |||
public Account User1 { get; set; } | |||
[JsonProperty("user2")] | |||
public Account User2 { get; set; } | |||
[JsonProperty("user3")] | |||
public Account User3 { get; set; } | |||
} | |||
} |
@@ -7,165 +7,484 @@ using System.Threading.Tasks; | |||
namespace Discord.Tests | |||
{ | |||
//TODO: Tests are massively incomplete and out of date, needing a full rewrite | |||
//TODO: Tests are massively incomplete and out of date, needing a full rewrite | |||
[TestClass] | |||
public class Tests | |||
{ | |||
private const int EventTimeout = 10000; //Max time in milliseconds to wait for an event response from our test actions | |||
[TestClass] | |||
public class Tests | |||
{ | |||
private const int EventTimeout = 5000; //Max time in milliseconds to wait for an event response from our test actions | |||
private static DiscordClient _hostClient, _targetBot, _observerBot; | |||
private static Server _testServer; | |||
private static Channel _testServerChannel; | |||
private static Random _random; | |||
[ClassInitialize] | |||
public static void Initialize(TestContext testContext) | |||
{ | |||
var settings = Settings.Instance; | |||
_random = new Random(); | |||
_hostClient = new DiscordClient(); | |||
_targetBot = new DiscordClient(); | |||
_observerBot = new DiscordClient(); | |||
_hostClient.Connect(settings.User1.Email, settings.User1.Password).Wait(); | |||
_targetBot.Connect(settings.User2.Email, settings.User2.Password).Wait(); | |||
_observerBot.Connect(settings.User3.Email, settings.User3.Password).Wait(); | |||
//Cleanup existing servers | |||
WaitMany( | |||
_hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()), | |||
_targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()), | |||
_observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave())); | |||
//Create new server and invite the other bots to it | |||
_testServer = _hostClient.CreateServer("Discord.Net Testing", _hostClient.Regions.First()).Result; | |||
_testServerChannel = _testServer.DefaultChannel; | |||
Invite invite = _testServer.CreateInvite(60, 1, false, false).Result; | |||
WaitAll( | |||
_targetBot.GetInvite(invite.Code).Result.Accept(), | |||
_observerBot.GetInvite(invite.Code).Result.Accept()); | |||
} | |||
//Channels | |||
[TestMethod] | |||
public void TestCreateTextChannel() | |||
=> TestCreateChannel(ChannelType.Text); | |||
[TestMethod] | |||
public void TestCreateVoiceChannel() | |||
=> TestCreateChannel(ChannelType.Voice); | |||
private void TestCreateChannel(ChannelType type) | |||
{ | |||
Channel channel = null; | |||
string name = $"#test_{_random.Next()}"; | |||
AssertEvent<ChannelEventArgs>( | |||
"ChannelCreated event never received", | |||
async () => channel = await _testServer.CreateChannel(name.Substring(1), type), | |||
x => _targetBot.ChannelCreated += x, | |||
x => _targetBot.ChannelCreated -= x, | |||
(s, e) => e.Channel.Name == name); | |||
AssertEvent<ChannelEventArgs>( | |||
"ChannelDestroyed event never received", | |||
async () => await channel.Delete(), | |||
x => _targetBot.ChannelDestroyed += x, | |||
x => _targetBot.ChannelDestroyed -= x, | |||
(s, e) => e.Channel.Name == name); | |||
} | |||
[TestMethod] | |||
[ExpectedException(typeof(InvalidOperationException))] | |||
public async Task TestCreateChannel_NoName() | |||
{ | |||
await _testServer.CreateChannel($"", ChannelType.Text); | |||
} | |||
[TestMethod] | |||
[ExpectedException(typeof(InvalidOperationException))] | |||
public async Task TestCreateChannel_NoType() | |||
{ | |||
string name = $"#test_{_random.Next()}"; | |||
await _testServer.CreateChannel($"", ChannelType.FromString("")); | |||
} | |||
[TestMethod] | |||
[ExpectedException(typeof(InvalidOperationException))] | |||
public async Task TestCreateChannel_BadType() | |||
{ | |||
string name = $"#test_{_random.Next()}"; | |||
await _testServer.CreateChannel($"", ChannelType.FromString("badtype")); | |||
} | |||
//Messages | |||
[TestMethod] | |||
public void TestSendMessage() | |||
{ | |||
string text = $"test_{_random.Next()}"; | |||
AssertEvent<MessageEventArgs>( | |||
"MessageCreated event never received", | |||
() => _testServerChannel.SendMessage(text), | |||
x => _targetBot.MessageReceived += x, | |||
x => _targetBot.MessageReceived -= x, | |||
(s, e) => e.Message.Text == text); | |||
} | |||
[ClassCleanup] | |||
public static void Cleanup() | |||
{ | |||
WaitMany( | |||
_hostClient.State == ConnectionState.Connected ? _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, | |||
_targetBot.State == ConnectionState.Connected ? _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, | |||
_observerBot.State == ConnectionState.Connected ? _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null); | |||
WaitAll( | |||
_hostClient.Disconnect(), | |||
_targetBot.Disconnect(), | |||
_observerBot.Disconnect()); | |||
} | |||
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null) | |||
{ | |||
AssertEvent(msg, action, addEvent, removeEvent, test, true); | |||
} | |||
private static void AssertNoEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null) | |||
{ | |||
AssertEvent(msg, action, addEvent, removeEvent, test, false); | |||
} | |||
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test, bool assertTrue) | |||
{ | |||
ManualResetEventSlim trigger = new ManualResetEventSlim(false); | |||
bool result = false; | |||
EventHandler<TArgs> handler = (s, e) => | |||
{ | |||
if (test != null) | |||
{ | |||
result |= test(s, e); | |||
trigger.Set(); | |||
private static Server _testServer; | |||
private static Channel _testServerChannel; | |||
private static Random _random; | |||
private static Invite _testServerInvite; | |||
private static TestContext _context; | |||
private static string HostBotToken; | |||
private static string ObserverBotToken; | |||
private static string TargetEmail; | |||
private static string TargetPassword; | |||
public static string RandomText => $"test_{_random.Next()}"; | |||
#region Initialization | |||
[ClassInitialize] | |||
public static void Initialize(TestContext testContext) | |||
{ | |||
_context = testContext; | |||
HostBotToken = Environment.GetEnvironmentVariable("discord-unit-host_token"); | |||
ObserverBotToken = Environment.GetEnvironmentVariable("discord-unit-observer_token"); | |||
TargetEmail = Environment.GetEnvironmentVariable("discord-unit-target_email"); | |||
TargetPassword = Environment.GetEnvironmentVariable("discord-unit-target_pass"); | |||
} | |||
[TestMethod] | |||
[Priority(1)] | |||
public async Task TestInitialize() | |||
{ | |||
_context.WriteLine("Initializing."); | |||
_random = new Random(); | |||
_hostClient = new DiscordClient(); | |||
_targetBot = new DiscordClient(); | |||
_observerBot = new DiscordClient(); | |||
await _hostClient.Connect(HostBotToken, TokenType.Bot); | |||
await Task.Delay(3000); | |||
//Cleanup existing servers | |||
_hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()); | |||
//Create new server and invite the other bots to it | |||
_testServer = await _hostClient.CreateServer("Discord.Net Testing", _hostClient.Regions.First()); | |||
await Task.Delay(1000); | |||
Invite invite = await _testServer.CreateInvite(60, 3, false, false); | |||
_testServerInvite = invite; | |||
_context.WriteLine($"Host: {_hostClient.CurrentUser.Name} in {_hostClient.Servers.Count()}"); | |||
} | |||
[TestMethod] | |||
[Priority(2)] | |||
public async Task TestTokenLogin_Ready() | |||
{ | |||
AssertEvent( | |||
"READY never received", | |||
async () => await _observerBot.Connect(ObserverBotToken, TokenType.Bot), | |||
x => _observerBot.Ready += x, | |||
x => _observerBot.Ready -= x, | |||
null, | |||
true); | |||
_observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()); | |||
await (await _observerBot.GetInvite(_testServerInvite.Code)).Accept(); | |||
} | |||
[TestMethod] | |||
[Priority(2)] | |||
public void TestReady() | |||
{ | |||
AssertEvent( | |||
"READY never received", | |||
async () => await _targetBot.Connect(TargetEmail, TargetPassword), | |||
x => _targetBot.Ready += x, | |||
x => _targetBot.Ready -= x, | |||
null, | |||
true); | |||
_targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()); | |||
_testServerChannel = _testServer.DefaultChannel; | |||
} | |||
#endregion | |||
// Servers | |||
#region Server Tests | |||
[TestMethod] | |||
[Priority(3)] | |||
public void TestJoinedServer() | |||
{ | |||
AssertEvent<ServerEventArgs>( | |||
"Never Got JoinedServer", | |||
async () => await (await _targetBot.GetInvite(_testServerInvite.Code)).Accept(), | |||
x => _targetBot.JoinedServer += x, | |||
x => _targetBot.JoinedServer -= x); | |||
} | |||
#endregion | |||
#region Channel Tests | |||
//Channels | |||
[TestMethod] | |||
public void TestCreateTextChannel() | |||
=> TestCreateChannel(ChannelType.Text); | |||
[TestMethod] | |||
public void TestCreateVoiceChannel() | |||
=> TestCreateChannel(ChannelType.Voice); | |||
private void TestCreateChannel(ChannelType type) | |||
{ | |||
_context.WriteLine($"Host: {_hostClient.CurrentUser.Name} in {_hostClient.Servers.Count()}"); | |||
_context.WriteLine($"Target: {_targetBot.CurrentUser.Name} in {_targetBot.Servers.Count()}"); | |||
_context.WriteLine($"Observer: {_observerBot.CurrentUser.Name} in {_observerBot.Servers.Count()}"); | |||
Channel channel = null; | |||
string name = $"test_{_random.Next()}"; | |||
AssertEvent<ChannelEventArgs>( | |||
"ChannelCreated event never received", | |||
async () => channel = await _testServer.CreateChannel(name, type), | |||
x => _targetBot.ChannelCreated += x, | |||
x => _targetBot.ChannelCreated -= x, | |||
(s, e) => e.Channel.Name == name); | |||
AssertEvent<ChannelEventArgs>( | |||
"ChannelDestroyed event never received", | |||
async () => await channel.Delete(), | |||
x => _targetBot.ChannelDestroyed += x, | |||
x => _targetBot.ChannelDestroyed -= x, | |||
(s, e) => e.Channel.Name == name); | |||
} | |||
[TestMethod] | |||
[ExpectedException(typeof(Net.HttpException))] | |||
public async Task TestCreateChannel_NoName() | |||
{ | |||
await _testServer.CreateChannel($"", ChannelType.Text); | |||
} | |||
[TestMethod] | |||
[ExpectedException(typeof(Net.HttpException))] | |||
public async Task TestCreateChannel_NoType() | |||
{ | |||
string name = $"#test_{_random.Next()}"; | |||
await _testServer.CreateChannel($"", ChannelType.FromString("")); | |||
} | |||
[TestMethod] | |||
[ExpectedException(typeof(Net.HttpException))] | |||
public async Task TestCreateChannel_BadType() | |||
{ | |||
string name = $"#test_{_random.Next()}"; | |||
await _testServer.CreateChannel($"", ChannelType.FromString("badtype")); | |||
} | |||
[TestMethod] | |||
public async Task Test_CreateGetChannel() | |||
{ | |||
var name = $"test_{_random.Next()}"; | |||
var channel = await _testServer.CreateChannel(name, ChannelType.Text); | |||
var get_channel = _testServer.GetChannel(channel.Id); | |||
Assert.AreEqual(channel.Id, get_channel.Id, "ID of Channel and GetChannel were not equal."); | |||
} | |||
[TestMethod] | |||
public void TestSendTyping() | |||
{ | |||
var channel = _testServerChannel; | |||
AssertEvent<ChannelUserEventArgs>( | |||
"UserUpdated event never fired.", | |||
async () => await channel.SendIsTyping(), | |||
x => _targetBot.UserIsTyping += x, | |||
x => _targetBot.UserIsTyping -= x); | |||
} | |||
[TestMethod] | |||
public void TestEditChannel() | |||
{ | |||
var channel = _testServerChannel; | |||
AssertEvent<ChannelUpdatedEventArgs>( | |||
"ChannelUpdated Never Received", | |||
async () => await channel.Edit(RandomText, $"topic - {RandomText}", 26), | |||
x => _targetBot.ChannelUpdated += x, | |||
x => _targetBot.ChannelUpdated -= x); | |||
} | |||
[TestMethod] | |||
public void TestChannelMention() | |||
{ | |||
var channel = _testServerChannel; | |||
Assert.AreEqual($"<#{channel.Id}>", channel.Mention, "Generated channel mention was not the expected channel mention."); | |||
} | |||
[TestMethod] | |||
public void TestChannelUserCount() | |||
{ | |||
Assert.AreEqual(3, _testServerChannel.Users.Count(), "Read an incorrect number of users in a channel"); | |||
} | |||
#endregion | |||
#region Message Tests | |||
//Messages | |||
[TestMethod] | |||
public void TestMessageEvents() | |||
{ | |||
string name = $"test_{_random.Next()}"; | |||
var channel = _testServer.CreateChannel(name, ChannelType.Text).Result; | |||
_context.WriteLine($"Channel Name: {channel.Name} / {channel.Server.Name}"); | |||
string text = $"test_{_random.Next()}"; | |||
Message message = null; | |||
AssertEvent<MessageEventArgs>( | |||
"MessageCreated event never received", | |||
async () => message = await channel.SendMessage(text), | |||
x => _targetBot.MessageReceived += x, | |||
x => _targetBot.MessageReceived -= x, | |||
(s, e) => e.Message.Text == text); | |||
AssertEvent<MessageUpdatedEventArgs>( | |||
"MessageUpdated event never received", | |||
async () => await message.Edit(text + " updated"), | |||
x => _targetBot.MessageUpdated += x, | |||
x => _targetBot.MessageUpdated -= x, | |||
(s, e) => e.Before.Text == text && e.After.Text == text + " updated"); | |||
AssertEvent<MessageEventArgs>( | |||
"MessageDeleted event never received", | |||
async () => await message.Delete(), | |||
x => _targetBot.MessageDeleted += x, | |||
x => _targetBot.MessageDeleted -= x, | |||
(s, e) => e.Message.Id == message.Id); | |||
} | |||
[TestMethod] | |||
public async Task TestDownloadMessages_WithCache() | |||
{ | |||
string name = $"test_{_random.Next()}"; | |||
var channel = await _testServer.CreateChannel(name, ChannelType.Text); | |||
for (var i = 0; i < 10; i++) await channel.SendMessage(RandomText); | |||
while (channel.Client.MessageQueue.Count > 0) await Task.Delay(100); | |||
var messages = await channel.DownloadMessages(10); | |||
Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10."); | |||
} | |||
[TestMethod] | |||
public async Task TestDownloadMessages_WithoutCache() | |||
{ | |||
string name = $"test_{_random.Next()}"; | |||
var channel = await _testServer.CreateChannel(name, ChannelType.Text); | |||
for (var i = 0; i < 10; i++) await channel.SendMessage(RandomText); | |||
while (channel.Client.MessageQueue.Count > 0) await Task.Delay(100); | |||
var messages = await channel.DownloadMessages(10, useCache: false); | |||
Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10."); | |||
} | |||
[TestMethod] | |||
public async Task TestSendTTSMessage() | |||
{ | |||
var channel = await _testServer.CreateChannel(RandomText, ChannelType.Text); | |||
AssertEvent<MessageEventArgs>( | |||
"MessageCreated event never fired", | |||
async () => await channel.SendTTSMessage(RandomText), | |||
x => _targetBot.MessageReceived += x, | |||
x => _targetBot.MessageReceived -= x, | |||
(s, e) => e.Message.IsTTS); | |||
} | |||
#endregion | |||
#region User Tests | |||
[TestMethod] | |||
public void TestUserMentions() | |||
{ | |||
var user = _targetBot.GetServer(_testServer.Id).CurrentUser; | |||
Assert.AreEqual($"<@{user.Id}>", user.Mention); | |||
} | |||
[TestMethod] | |||
public void TestUserEdit() | |||
{ | |||
var user = _testServer.GetUser(_targetBot.CurrentUser.Id); | |||
AssertEvent<UserUpdatedEventArgs>( | |||
"UserUpdated never fired", | |||
async () => await user.Edit(true, true, null, null), | |||
x => _targetBot.UserUpdated += x, | |||
x => _targetBot.UserUpdated -= x); | |||
} | |||
[TestMethod] | |||
public void TestEditSelf() | |||
{ | |||
var name = $"test_{_random.Next()}"; | |||
AssertEvent<UserUpdatedEventArgs>( | |||
"UserUpdated never fired", | |||
async () => await _targetBot.CurrentUser.Edit(TargetPassword, name), | |||
x => _observerBot.UserUpdated += x, | |||
x => _observerBot.UserUpdated -= x, | |||
(s, e) => e.After.Name == name); | |||
} | |||
[TestMethod] | |||
public void TestSetStatus() | |||
{ | |||
AssertEvent<UserUpdatedEventArgs>( | |||
"UserUpdated never fired", | |||
async () => await SetStatus(_targetBot, UserStatus.Idle), | |||
x => _observerBot.UserUpdated += x, | |||
x => _observerBot.UserUpdated -= x, | |||
(s, e) => e.After.Status == UserStatus.Idle); | |||
} | |||
private async Task SetStatus(DiscordClient _client, UserStatus status) | |||
{ | |||
_client.SetStatus(status); | |||
await Task.Delay(50); | |||
} | |||
[TestMethod] | |||
public void TestSetGame() | |||
{ | |||
AssertEvent<UserUpdatedEventArgs>( | |||
"UserUpdated never fired", | |||
async () => await SetGame(_targetBot, "test game"), | |||
x => _observerBot.UserUpdated += x, | |||
x => _observerBot.UserUpdated -= x, | |||
(s, e) => _targetBot.CurrentGame.Name == "test game"); | |||
} | |||
private async Task SetGame(DiscordClient _client, string game) | |||
{ | |||
_client.SetGame(game); | |||
await Task.Delay(5); | |||
} | |||
#endregion | |||
#region Permission Tests | |||
// Permissions | |||
[TestMethod] | |||
public async Task Test_AddGet_PermissionsRule() | |||
{ | |||
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text); | |||
var user = _testServer.GetUser(_targetBot.CurrentUser.Id); | |||
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny); | |||
await channel.AddPermissionsRule(user, perms); | |||
var resultPerms = channel.GetPermissionsRule(user); | |||
Assert.IsNotNull(resultPerms, "Perms retrieved from server were null."); | |||
} | |||
[TestMethod] | |||
public async Task Test_AddRemove_PermissionsRule() | |||
{ | |||
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text); | |||
var user = _testServer.GetUser(_targetBot.CurrentUser.Id); | |||
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny); | |||
await channel.AddPermissionsRule(user, perms); | |||
await channel.RemovePermissionsRule(user); | |||
await Task.Delay(200); | |||
Assert.AreEqual(PermValue.Inherit, channel.GetPermissionsRule(user).SendMessages); | |||
} | |||
[TestMethod] | |||
public async Task Test_Permissions_Event() | |||
{ | |||
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text); | |||
var user = _testServer.GetUser(_targetBot.CurrentUser.Id); | |||
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny); | |||
AssertEvent<ChannelUpdatedEventArgs> | |||
("ChannelUpdatedEvent never fired.", | |||
async () => await channel.AddPermissionsRule(user, perms), | |||
x => _targetBot.ChannelUpdated += x, | |||
x => _targetBot.ChannelUpdated -= x, | |||
(s, e) => e.After.PermissionOverwrites.Count() != e.Before.PermissionOverwrites.Count()); | |||
} | |||
[TestMethod] | |||
[ExpectedException(typeof(Net.HttpException))] | |||
public async Task Test_Affect_Permissions_Invalid_Channel() | |||
{ | |||
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text); | |||
var user = _testServer.GetUser(_targetBot.CurrentUser.Id); | |||
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny); | |||
await channel.Delete(); | |||
await channel.AddPermissionsRule(user, perms); | |||
} | |||
#endregion | |||
[ClassCleanup] | |||
public static void Cleanup() | |||
{ | |||
WaitMany( | |||
_hostClient.State == ConnectionState.Connected ? _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, | |||
_targetBot.State == ConnectionState.Connected ? _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, | |||
_observerBot.State == ConnectionState.Connected ? _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null); | |||
WaitAll( | |||
_hostClient.Disconnect(), | |||
_targetBot.Disconnect(), | |||
_observerBot.Disconnect()); | |||
} | |||
#region Helpers | |||
// Task Helpers | |||
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null) | |||
{ | |||
AssertEvent(msg, action, addEvent, removeEvent, test, true); | |||
} | |||
private static void AssertNoEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null) | |||
{ | |||
AssertEvent(msg, action, addEvent, removeEvent, test, false); | |||
} | |||
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test, bool assertTrue) | |||
{ | |||
ManualResetEventSlim trigger = new ManualResetEventSlim(false); | |||
bool result = false; | |||
EventHandler<TArgs> handler = (s, e) => | |||
{ | |||
if (test != null) | |||
{ | |||
result |= test(s, e); | |||
trigger.Set(); | |||
} | |||
else | |||
result = true; | |||
}; | |||
addEvent(handler); | |||
var task = action(); | |||
trigger.Wait(EventTimeout); | |||
task.Wait(); | |||
removeEvent(handler); | |||
Assert.AreEqual(assertTrue, result, msg); | |||
} | |||
private static void WaitAll(params Task[] tasks) | |||
{ | |||
Task.WaitAll(tasks); | |||
} | |||
private static void WaitAll(IEnumerable<Task> tasks) | |||
{ | |||
Task.WaitAll(tasks.ToArray()); | |||
} | |||
private static void WaitMany(params IEnumerable<Task>[] tasks) | |||
{ | |||
Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); | |||
} | |||
} | |||
else | |||
result = true; | |||
}; | |||
addEvent(handler); | |||
var task = action(); | |||
trigger.Wait(EventTimeout); | |||
task.Wait(); | |||
removeEvent(handler); | |||
Assert.AreEqual(assertTrue, result, msg); | |||
} | |||
private static void AssertEvent(string msg, Func<Task> action, Action<EventHandler> addEvent, Action<EventHandler> removeEvent, Func<object, bool> test, bool assertTrue) | |||
{ | |||
ManualResetEventSlim trigger = new ManualResetEventSlim(false); | |||
bool result = false; | |||
EventHandler handler = (s, e) => | |||
{ | |||
if (test != null) | |||
{ | |||
result |= test(s); | |||
trigger.Set(); | |||
} | |||
else | |||
result = true; | |||
}; | |||
addEvent(handler); | |||
var task = action(); | |||
trigger.Wait(EventTimeout); | |||
task.Wait(); | |||
removeEvent(handler); | |||
Assert.AreEqual(assertTrue, result, msg); | |||
} | |||
private static void WaitAll(params Task[] tasks) | |||
{ | |||
Task.WaitAll(tasks); | |||
} | |||
private static void WaitAll(IEnumerable<Task> tasks) | |||
{ | |||
Task.WaitAll(tasks.ToArray()); | |||
} | |||
private static void WaitMany(params IEnumerable<Task>[] tasks) | |||
{ | |||
Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); | |||
} | |||
#endregion | |||
} | |||
} |
@@ -1,4 +1,4 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<packages> | |||
<package id="Newtonsoft.Json" version="8.0.1" targetFramework="net45" /> | |||
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" /> | |||
</packages> |