Initial commit, basic proxying working

This commit is contained in:
Ske
2020-12-22 13:15:26 +01:00
parent c3f6becea4
commit a6fbd869be
109 changed files with 3539 additions and 359 deletions

88
Myriad/Gateway/Cluster.cs Normal file
View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Myriad.Types;
using Serilog;
namespace Myriad.Gateway
{
public class Cluster
{
private readonly GatewaySettings _gatewaySettings;
private readonly ILogger _logger;
private readonly ConcurrentDictionary<int, Shard> _shards = new();
public Cluster(GatewaySettings gatewaySettings, ILogger logger)
{
_gatewaySettings = gatewaySettings;
_logger = logger;
}
public Func<Shard, IGatewayEvent, Task>? EventReceived { get; set; }
public IReadOnlyDictionary<int, Shard> Shards => _shards;
public ClusterSessionState SessionState => GetClusterState();
public User? User => _shards.Values.Select(s => s.User).FirstOrDefault(s => s != null);
private ClusterSessionState GetClusterState()
{
var shards = new List<ClusterSessionState.ShardState>();
foreach (var (id, shard) in _shards)
shards.Add(new ClusterSessionState.ShardState
{
Shard = shard.ShardInfo ?? new ShardInfo(id, _shards.Count), Session = shard.SessionInfo
});
return new ClusterSessionState {Shards = shards};
}
public async Task Start(GatewayInfo.Bot info, ClusterSessionState? lastState = null)
{
if (lastState != null && lastState.Shards.Count == info.Shards)
await Resume(info.Url, lastState);
else
await Start(info.Url, info.Shards);
}
public async Task Resume(string url, ClusterSessionState sessionState)
{
_logger.Information("Resuming session with {ShardCount} shards at {Url}", sessionState.Shards.Count, url);
foreach (var shardState in sessionState.Shards)
CreateAndAddShard(url, shardState.Shard, shardState.Session);
await StartShards();
}
public async Task Start(string url, int shardCount)
{
_logger.Information("Starting {ShardCount} shards at {Url}", shardCount, url);
for (var i = 0; i < shardCount; i++)
CreateAndAddShard(url, new ShardInfo(i, shardCount), null);
await StartShards();
}
private async Task StartShards()
{
_logger.Information("Connecting shards...");
await Task.WhenAll(_shards.Values.Select(s => s.Start()));
}
private void CreateAndAddShard(string url, ShardInfo shardInfo, ShardSessionInfo? session)
{
var shard = new Shard(_logger, new Uri(url), _gatewaySettings, shardInfo, session);
shard.OnEventReceived += evt => OnShardEventReceived(shard, evt);
_shards[shardInfo.ShardId] = shard;
}
private async Task OnShardEventReceived(Shard shard, IGatewayEvent evt)
{
if (EventReceived != null)
await EventReceived(shard, evt);
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Myriad.Gateway
{
public record ClusterSessionState
{
public List<ShardState> Shards { get; init; }
public record ShardState
{
public ShardInfo Shard { get; init; }
public ShardSessionInfo Session { get; init; }
}
}
}

View File

@@ -0,0 +1,6 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record ChannelCreateEvent: Channel, IGatewayEvent;
}

View File

@@ -0,0 +1,6 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record ChannelDeleteEvent: Channel, IGatewayEvent;
}

View File

@@ -0,0 +1,6 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record ChannelUpdateEvent: Channel, IGatewayEvent;
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using Myriad.Types;
namespace Myriad.Gateway
{
public record GuildCreateEvent: Guild, IGatewayEvent
{
public Channel[] Channels { get; init; }
public GuildMember[] Members { get; init; }
}
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record GuildDeleteEvent(ulong Id, bool Unavailable): IGatewayEvent;
}

View File

@@ -0,0 +1,9 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record GuildMemberAddEvent: GuildMember, IGatewayEvent
{
public ulong GuildId { get; init; }
}
}

View File

@@ -0,0 +1,10 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public class GuildMemberRemoveEvent: IGatewayEvent
{
public ulong GuildId { get; init; }
public User User { get; init; }
}
}

View File

@@ -0,0 +1,9 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record GuildMemberUpdateEvent: GuildMember, IGatewayEvent
{
public ulong GuildId { get; init; }
}
}

View File

@@ -0,0 +1,6 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record GuildRoleCreateEvent(ulong GuildId, Role Role): IGatewayEvent;
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record GuildRoleDeleteEvent(ulong GuildId, ulong RoleId): IGatewayEvent;
}

View File

@@ -0,0 +1,6 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record GuildRoleUpdateEvent(ulong GuildId, Role Role): IGatewayEvent;
}

View File

@@ -0,0 +1,6 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record GuildUpdateEvent: Guild, IGatewayEvent;
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
namespace Myriad.Gateway
{
public interface IGatewayEvent
{
public static readonly Dictionary<string, Type> EventTypes = new()
{
{"READY", typeof(ReadyEvent)},
{"RESUMED", typeof(ResumedEvent)},
{"GUILD_CREATE", typeof(GuildCreateEvent)},
{"GUILD_UPDATE", typeof(GuildUpdateEvent)},
{"GUILD_DELETE", typeof(GuildDeleteEvent)},
{"GUILD_MEMBER_ADD", typeof(GuildMemberAddEvent)},
{"GUILD_MEMBER_REMOVE", typeof(GuildMemberRemoveEvent)},
{"GUILD_MEMBER_UPDATE", typeof(GuildMemberUpdateEvent)},
{"GUILD_ROLE_CREATE", typeof(GuildRoleCreateEvent)},
{"GUILD_ROLE_UPDATE", typeof(GuildRoleUpdateEvent)},
{"GUILD_ROLE_DELETE", typeof(GuildRoleDeleteEvent)},
{"CHANNEL_CREATE", typeof(ChannelCreateEvent)},
{"CHANNEL_UPDATE", typeof(ChannelUpdateEvent)},
{"CHANNEL_DELETE", typeof(ChannelDeleteEvent)},
{"MESSAGE_CREATE", typeof(MessageCreateEvent)},
{"MESSAGE_UPDATE", typeof(MessageUpdateEvent)},
{"MESSAGE_DELETE", typeof(MessageDeleteEvent)},
{"MESSAGE_DELETE_BULK", typeof(MessageDeleteBulkEvent)},
{"MESSAGE_REACTION_ADD", typeof(MessageReactionAddEvent)},
{"MESSAGE_REACTION_REMOVE", typeof(MessageReactionRemoveEvent)},
{"MESSAGE_REACTION_REMOVE_ALL", typeof(MessageReactionRemoveAllEvent)},
{"MESSAGE_REACTION_REMOVE_EMOJI", typeof(MessageReactionRemoveEmojiEvent)},
{"INTERACTION_CREATE", typeof(InteractionCreateEvent)}
};
}
}

View File

@@ -0,0 +1,6 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record InteractionCreateEvent: Interaction, IGatewayEvent;
}

View File

@@ -0,0 +1,9 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record MessageCreateEvent: Message, IGatewayEvent
{
public GuildMemberPartial? Member { get; init; }
}
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record MessageDeleteBulkEvent(ulong[] Ids, ulong ChannelId, ulong? GuildId): IGatewayEvent;
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record MessageDeleteEvent(ulong Id, ulong ChannelId, ulong? GuildId): IGatewayEvent;
}

View File

@@ -0,0 +1,8 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record MessageReactionAddEvent(ulong UserId, ulong ChannelId, ulong MessageId, ulong? GuildId,
GuildMember? Member,
Emoji Emoji): IGatewayEvent;
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record MessageReactionRemoveAllEvent(ulong ChannelId, ulong MessageId, ulong? GuildId): IGatewayEvent;
}

View File

@@ -0,0 +1,7 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record MessageReactionRemoveEmojiEvent
(ulong ChannelId, ulong MessageId, ulong? GuildId, Emoji Emoji): IGatewayEvent;
}

View File

@@ -0,0 +1,7 @@
using Myriad.Types;
namespace Myriad.Gateway
{
public record MessageReactionRemoveEvent
(ulong UserId, ulong ChannelId, ulong MessageId, ulong? GuildId, Emoji Emoji): IGatewayEvent;
}

View File

@@ -0,0 +1,7 @@
namespace Myriad.Gateway
{
public record MessageUpdateEvent(ulong Id, ulong ChannelId): IGatewayEvent
{
// TODO: lots of partials
}
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
using Myriad.Types;
namespace Myriad.Gateway
{
public record ReadyEvent: IGatewayEvent
{
[JsonPropertyName("v")] public int Version { get; init; }
public User User { get; init; }
public string SessionId { get; init; }
public ShardInfo? Shard { get; init; }
public ApplicationPartial Application { get; init; }
}
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record ResumedEvent: IGatewayEvent;
}

View File

@@ -0,0 +1,35 @@
using System;
namespace Myriad.Gateway
{
// TODO: unused?
public class GatewayCloseException: Exception
{
public GatewayCloseException(int closeCode, string closeReason): base($"{closeCode}: {closeReason}")
{
CloseCode = closeCode;
CloseReason = closeReason;
}
public int CloseCode { get; }
public string CloseReason { get; }
}
public class GatewayCloseCode
{
public const int UnknownError = 4000;
public const int UnknownOpcode = 4001;
public const int DecodeError = 4002;
public const int NotAuthenticated = 4003;
public const int AuthenticationFailed = 4004;
public const int AlreadyAuthenticated = 4005;
public const int InvalidSeq = 4007;
public const int RateLimited = 4008;
public const int SessionTimedOut = 4009;
public const int InvalidShard = 4010;
public const int ShardingRequired = 4011;
public const int InvalidApiVersion = 4012;
public const int InvalidIntent = 4013;
public const int DisallowedIntent = 4014;
}
}

View File

@@ -0,0 +1,24 @@
using System;
namespace Myriad.Gateway
{
[Flags]
public enum GatewayIntent
{
Guilds = 1 << 0,
GuildMembers = 1 << 1,
GuildBans = 1 << 2,
GuildEmojis = 1 << 3,
GuildIntegrations = 1 << 4,
GuildWebhooks = 1 << 5,
GuildInvites = 1 << 6,
GuildVoiceStates = 1 << 7,
GuildPresences = 1 << 8,
GuildMessages = 1 << 9,
GuildMessageReactions = 1 << 10,
GuildMessageTyping = 1 << 11,
DirectMessages = 1 << 12,
DirectMessageReactions = 1 << 13,
DirectMessageTyping = 1 << 14
}
}

View File

@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
namespace Myriad.Gateway
{
public record GatewayPacket
{
[JsonPropertyName("op")] public GatewayOpcode Opcode { get; init; }
[JsonPropertyName("d")] public object? Payload { get; init; }
[JsonPropertyName("s")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Sequence { get; init; }
[JsonPropertyName("t")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? EventType { get; init; }
}
public enum GatewayOpcode
{
Dispatch = 0,
Heartbeat = 1,
Identify = 2,
PresenceUpdate = 3,
VoiceStateUpdate = 4,
Resume = 6,
Reconnect = 7,
RequestGuildMembers = 8,
InvalidSession = 9,
Hello = 10,
HeartbeatAck = 11
}
}

View File

@@ -0,0 +1,8 @@
namespace Myriad.Gateway
{
public record GatewaySettings
{
public string Token { get; init; }
public GatewayIntent Intents { get; init; }
}
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record GatewayHello(int HeartbeatInterval);
}

View File

@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace Myriad.Gateway
{
public record GatewayIdentify
{
public string Token { get; init; }
public ConnectionProperties Properties { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Compress { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? LargeThreshold { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ShardInfo? Shard { get; init; }
public GatewayIntent Intents { get; init; }
public record ConnectionProperties
{
[JsonPropertyName("$os")] public string Os { get; init; }
[JsonPropertyName("$browser")] public string Browser { get; init; }
[JsonPropertyName("$device")] public string Device { get; init; }
}
}
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record GatewayResume(string Token, string SessionId, int Seq);
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using Myriad.Types;
namespace Myriad.Gateway
{
public record GatewayStatusUpdate
{
public enum UserStatus
{
Online,
Dnd,
Idle,
Invisible,
Offline
}
public ulong? Since { get; init; }
public ActivityPartial[]? Activities { get; init; }
public UserStatus Status { get; init; }
public bool Afk { get; init; }
}
}

328
Myriad/Gateway/Shard.cs Normal file
View File

@@ -0,0 +1,328 @@
using System;
using System.Net.WebSockets;
using System.Text.Json;
using System.Threading.Tasks;
using Myriad.Serialization;
using Myriad.Types;
using Serilog;
namespace Myriad.Gateway
{
public class Shard: IAsyncDisposable
{
private const string LibraryName = "Newcord Test";
private readonly JsonSerializerOptions _jsonSerializerOptions =
new JsonSerializerOptions().ConfigureForNewcord();
private readonly ILogger _logger;
private readonly Uri _uri;
private ShardConnection? _conn;
private TimeSpan? _currentHeartbeatInterval;
private bool _hasReceivedAck;
private DateTimeOffset? _lastHeartbeatSent;
private Task _worker;
public ShardInfo? ShardInfo { get; private set; }
public GatewaySettings Settings { get; }
public ShardSessionInfo SessionInfo { get; private set; }
public ShardState State { get; private set; }
public TimeSpan? Latency { get; private set; }
public User? User { get; private set; }
public Func<IGatewayEvent, Task>? OnEventReceived { get; set; }
public Shard(ILogger logger, Uri uri, GatewaySettings settings, ShardInfo? info = null,
ShardSessionInfo? sessionInfo = null)
{
_logger = logger;
_uri = uri;
Settings = settings;
ShardInfo = info;
SessionInfo = sessionInfo ?? new ShardSessionInfo();
}
public async ValueTask DisposeAsync()
{
if (_conn != null)
await _conn.DisposeAsync();
}
public Task Start()
{
_worker = MainLoop();
return Task.CompletedTask;
}
public async Task UpdateStatus(GatewayStatusUpdate payload)
{
if (_conn != null && _conn.State == WebSocketState.Open)
await _conn!.Send(new GatewayPacket {Opcode = GatewayOpcode.PresenceUpdate, Payload = payload});
}
private async Task MainLoop()
{
while (true)
try
{
_logger.Information("Connecting...");
State = ShardState.Connecting;
await Connect();
_logger.Information("Connected. Entering main loop...");
// Tick returns false if we need to stop and reconnect
while (await Tick(_conn!))
await Task.Delay(TimeSpan.FromMilliseconds(1000));
_logger.Information("Connection closed, reconnecting...");
State = ShardState.Closed;
}
catch (Exception e)
{
_logger.Error(e, "Error in shard state handler");
}
}
private async Task<bool> Tick(ShardConnection conn)
{
if (conn.State != WebSocketState.Connecting && conn.State != WebSocketState.Open)
return false;
if (!await TickHeartbeat(conn))
// TickHeartbeat returns false if we're disconnecting
return false;
return true;
}
private async Task<bool> TickHeartbeat(ShardConnection conn)
{
// If we don't need to heartbeat, do nothing
if (_lastHeartbeatSent == null || _currentHeartbeatInterval == null)
return true;
if (DateTimeOffset.UtcNow - _lastHeartbeatSent < _currentHeartbeatInterval)
return true;
// If we haven't received the ack in time, close w/ error
if (!_hasReceivedAck)
{
_logger.Warning(
"Did not receive heartbeat Ack from gateway within interval ({HeartbeatInterval})",
_currentHeartbeatInterval);
State = ShardState.Closing;
await conn.Disconnect(WebSocketCloseStatus.ProtocolError, "Did not receive ACK in time");
return false;
}
// Otherwise just send it :)
await SendHeartbeat(conn);
_hasReceivedAck = false;
return true;
}
private async Task SendHeartbeat(ShardConnection conn)
{
_logger.Debug("Sending heartbeat");
await conn.Send(new GatewayPacket {Opcode = GatewayOpcode.Heartbeat, Payload = SessionInfo.LastSequence});
_lastHeartbeatSent = DateTimeOffset.UtcNow;
}
private async Task Connect()
{
if (_conn != null)
await _conn.DisposeAsync();
_currentHeartbeatInterval = null;
_conn = new ShardConnection(_uri, _logger, _jsonSerializerOptions) {OnReceive = OnReceive};
}
private async Task OnReceive(GatewayPacket packet)
{
switch (packet.Opcode)
{
case GatewayOpcode.Hello:
{
await HandleHello((JsonElement) packet.Payload!);
break;
}
case GatewayOpcode.Heartbeat:
{
_logger.Debug("Received heartbeat request from shard, sending Ack");
await _conn!.Send(new GatewayPacket {Opcode = GatewayOpcode.HeartbeatAck});
break;
}
case GatewayOpcode.HeartbeatAck:
{
Latency = DateTimeOffset.UtcNow - _lastHeartbeatSent;
_logger.Debug("Received heartbeat Ack (latency {Latency})", Latency);
_hasReceivedAck = true;
break;
}
case GatewayOpcode.Reconnect:
{
_logger.Information("Received Reconnect, closing and reconnecting");
await _conn!.Disconnect(WebSocketCloseStatus.Empty, null);
break;
}
case GatewayOpcode.InvalidSession:
{
var canResume = ((JsonElement) packet.Payload!).GetBoolean();
// Clear session info before DCing
if (!canResume)
SessionInfo = SessionInfo with { Session = null };
var delay = TimeSpan.FromMilliseconds(new Random().Next(1000, 5000));
_logger.Information(
"Received Invalid Session (can resume? {CanResume}), reconnecting after {ReconnectDelay}",
canResume, delay);
await _conn!.Disconnect(WebSocketCloseStatus.Empty, null);
// Will reconnect after exiting this "loop"
await Task.Delay(delay);
break;
}
case GatewayOpcode.Dispatch:
{
SessionInfo = SessionInfo with { LastSequence = packet.Sequence };
var evt = DeserializeEvent(packet.EventType!, (JsonElement) packet.Payload!)!;
if (evt is ReadyEvent rdy)
{
if (State == ShardState.Connecting)
await HandleReady(rdy);
else
_logger.Warning("Received Ready event in unexpected state {ShardState}, ignoring?", State);
}
else if (evt is ResumedEvent)
{
if (State == ShardState.Connecting)
await HandleResumed();
else
_logger.Warning("Received Resumed event in unexpected state {ShardState}, ignoring?",
State);
}
await HandleEvent(evt);
break;
}
default:
{
_logger.Debug("Received unknown gateway opcode {Opcode}", packet.Opcode);
break;
}
}
}
private async Task HandleEvent(IGatewayEvent evt)
{
if (OnEventReceived != null)
await OnEventReceived.Invoke(evt);
}
private IGatewayEvent? DeserializeEvent(string eventType, JsonElement data)
{
if (!IGatewayEvent.EventTypes.TryGetValue(eventType, out var clrType))
{
_logger.Information("Received unknown event type {EventType}", eventType);
return null;
}
try
{
_logger.Verbose("Deserializing {EventType} to {ClrType}", eventType, clrType);
return JsonSerializer.Deserialize(data.GetRawText(), clrType, _jsonSerializerOptions)
as IGatewayEvent;
}
catch (JsonException e)
{
_logger.Error(e, "Error deserializing event {EventType} to {ClrType}", eventType, clrType);
return null;
}
}
private Task HandleReady(ReadyEvent ready)
{
ShardInfo = ready.Shard;
SessionInfo = SessionInfo with { Session = ready.SessionId };
User = ready.User;
State = ShardState.Open;
return Task.CompletedTask;
}
private Task HandleResumed()
{
State = ShardState.Open;
return Task.CompletedTask;
}
private async Task HandleHello(JsonElement json)
{
var hello = JsonSerializer.Deserialize<GatewayHello>(json.GetRawText(), _jsonSerializerOptions)!;
_logger.Debug("Received Hello with interval {Interval} ms", hello.HeartbeatInterval);
_currentHeartbeatInterval = TimeSpan.FromMilliseconds(hello.HeartbeatInterval);
await SendHeartbeat(_conn!);
await SendIdentifyOrResume();
}
private async Task SendIdentifyOrResume()
{
if (SessionInfo.Session != null && SessionInfo.LastSequence != null)
await SendResume(SessionInfo.Session, SessionInfo.LastSequence!.Value);
else
await SendIdentify();
}
private async Task SendIdentify()
{
_logger.Information("Sending gateway Identify for shard {@ShardInfo}", SessionInfo);
await _conn!.Send(new GatewayPacket
{
Opcode = GatewayOpcode.Identify,
Payload = new GatewayIdentify
{
Token = Settings.Token,
Properties = new GatewayIdentify.ConnectionProperties
{
Browser = LibraryName, Device = LibraryName, Os = Environment.OSVersion.ToString()
},
Intents = Settings.Intents,
Shard = ShardInfo
}
});
}
private async Task SendResume(string session, int lastSequence)
{
_logger.Information("Sending gateway Resume for session {@SessionInfo}", ShardInfo,
SessionInfo);
await _conn!.Send(new GatewayPacket
{
Opcode = GatewayOpcode.Resume, Payload = new GatewayResume(Settings.Token, session, lastSequence)
});
}
public enum ShardState
{
Closed,
Connecting,
Open,
Closing
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Buffers;
using System.IO;
using System.Net.WebSockets;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
namespace Myriad.Gateway
{
public class ShardConnection: IAsyncDisposable
{
private readonly MemoryStream _bufStream = new();
private readonly ClientWebSocket _client = new();
private readonly CancellationTokenSource _cts = new();
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly ILogger _logger;
private readonly Task _worker;
public ShardConnection(Uri uri, ILogger logger, JsonSerializerOptions jsonSerializerOptions)
{
_logger = logger;
_jsonSerializerOptions = jsonSerializerOptions;
_worker = Worker(uri);
}
public Func<GatewayPacket, Task>? OnReceive { get; set; }
public WebSocketState State => _client.State;
public async ValueTask DisposeAsync()
{
_cts.Cancel();
await _worker;
_client.Dispose();
await _bufStream.DisposeAsync();
_cts.Dispose();
}
private async Task Worker(Uri uri)
{
var realUrl = new UriBuilder(uri)
{
Query = "v=8&encoding=json"
}.Uri;
_logger.Debug("Connecting to gateway WebSocket at {GatewayUrl}", realUrl);
await _client.ConnectAsync(realUrl, default);
while (!_cts.IsCancellationRequested && _client.State == WebSocketState.Open)
try
{
await HandleReceive();
}
catch (Exception e)
{
_logger.Error(e, "Error in WebSocket receive worker");
}
}
private async Task HandleReceive()
{
_bufStream.SetLength(0);
var result = await ReadData(_bufStream);
var data = _bufStream.GetBuffer().AsMemory(0, (int) _bufStream.Position);
if (result.MessageType == WebSocketMessageType.Text)
await HandleReceiveData(data);
else if (result.MessageType == WebSocketMessageType.Close)
_logger.Information("WebSocket closed by server: {StatusCode} {Reason}", _client.CloseStatus,
_client.CloseStatusDescription);
}
private async Task HandleReceiveData(Memory<byte> data)
{
var packet = JsonSerializer.Deserialize<GatewayPacket>(data.Span, _jsonSerializerOptions)!;
try
{
if (OnReceive != null)
await OnReceive.Invoke(packet);
}
catch (Exception e)
{
_logger.Error(e, "Error in gateway handler for {OpcodeType}", packet.Opcode);
}
}
private async Task<ValueWebSocketReceiveResult> ReadData(MemoryStream stream)
{
using var buf = MemoryPool<byte>.Shared.Rent();
ValueWebSocketReceiveResult result;
do
{
result = await _client.ReceiveAsync(buf.Memory, _cts.Token);
stream.Write(buf.Memory.Span.Slice(0, result.Count));
} while (!result.EndOfMessage);
return result;
}
public async Task Send(GatewayPacket packet)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(packet, _jsonSerializerOptions);
await _client.SendAsync(bytes.AsMemory(), WebSocketMessageType.Text, true, default);
}
public async Task Disconnect(WebSocketCloseStatus status, string? description)
{
await _client.CloseAsync(status, description, default);
_cts.Cancel();
}
}
}

View File

@@ -0,0 +1,4 @@
namespace Myriad.Gateway
{
public record ShardInfo(int ShardId, int NumShards);
}

View File

@@ -0,0 +1,8 @@
namespace Myriad.Gateway
{
public record ShardSessionInfo
{
public string? Session { get; init; }
public int? LastSequence { get; init; }
}
}