using Maki.Structures.Channels;
using Maki.Structures.Gateway;
using Maki.Structures.Guilds;
using Maki.Structures.Messages;
using Maki.Structures.Presences;
using Maki.Structures.Users;
using Newtonsoft.Json;
using System;
using WebSocketSharp;
namespace Maki.Gateway
{
///
/// Gateway connection shard
///
class GatewayShard : IDisposable
{
///
/// Session key for continuing a resuming after disconnecting
///
private string session;
///
/// Websocket container
///
private WebSocket webSocket;
///
/// Active gateway version
///
private int gatewayVersion = Discord.GATEWAY_VERSION;
///
/// Interval at which heartbeats are sent
///
public TimeSpan HeartbeatInterval { get; private set; }
///
/// Last sequence received from the gateway, primarily used for resuming connections and getting the data that was missed
///
public int? LastSequence { get; private set; } = null;
///
/// Identifier for this Shard
///
public readonly int Id;
///
/// Parent DiscordClient instance
///
private readonly Discord client;
///
/// Heartbeat handler
///
private readonly GatewayHeartbeatManager heartbeatHandler;
#region Events
public event Action OnCallCreate;
public event Action OnCallDelete;
public event Action OnCallUpdate;
public event Action OnVoiceServerUpdate;
public event Action OnVoiceStateUpdate;
public event Action OnChannelCreate;
public event Action OnChannelDelete;
public event Action OnChannelPinsAck;
public event Action OnChannelPinsUpdate;
public event Action OnChannelRecipientAdd;
public event Action OnChannelRecipientRemove;
public event Action OnChannelUpdate;
public event Action OnGuildBanAdd;
public event Action OnGuildBanRemove;
public event Action OnGuildCreate;
public event Action OnGuildDelete;
public event Action OnGuildEmojisUpdate;
public event Action OnGuildIntegrationsUpdate;
public event Action OnGuildMemberAdd;
public event Action OnGuildMemberUpdate;
public event Action OnGuildMemberRemove;
public event Action OnGuildMembersChunk;
public event Action OnGuildRoleCreate;
public event Action OnGuildRoleUpdate;
public event Action OnGuildRoleDelete;
public event Action OnGuildUpdate;
public event Action OnMessageAck;
public event Action OnMessageCreate;
public event Action OnMessageDelete;
public event Action OnMessageDeleteBulk;
public event Action OnMessageReactionAdd;
public event Action OnMessageReactionRemove;
public event Action OnMessageReactionsRemoveAll;
public event Action OnMessageUpdate;
public event Action OnTypingStart;
public event Action OnPresenceUpdate;
public event Action OnPresencesReplace;
public event Action OnFriendSuggestionDelete;
public event Action OnRelationshipAdd;
public event Action OnRelationshipRemove;
public event Action OnReady;
public event Action OnResumed;
public event Action OnUserUpdate;
public event Action OnUserNoteUpdate;
public event Action OnUserSettingsUpdate;
public event Action OnSocketOpen;
public event Action OnSocketClose;
public event Action OnSocketError;
public event Action OnSocketMessage;
#endregion
///
/// Constructor
///
/// Shard Id
/// Parent DiscordClient instance
public GatewayShard(int id, Discord c)
{
Id = id;
client = c;
heartbeatHandler = new GatewayHeartbeatManager(this);
Connect();
}
///
/// Event handler for WebSocketSharp's OnOpen event, forwards to our OnSocketOpen event
///
/// Sender object
/// Event arguments
private void WebSocket_OnOpen(object sender, EventArgs e) => OnSocketOpen?.Invoke(this);
///
/// Event handler for WebSocketSharp's OnError event, forwards to our OnSocketError event
///
/// Sender object
/// Event arguments
private void WebSocket_OnError(object sender, ErrorEventArgs e) => OnSocketError?.Invoke(this, e.Exception);
///
/// Event handler for WebSocketSharp's OnClose event, stops heartbeats, forwards to our OnSocketClose event and reconnects if the close wasn't clean
///
/// Sender object
/// Event arguments
private void WebSocket_OnClose(object sender, CloseEventArgs e)
{
heartbeatHandler.Stop();
OnSocketClose?.Invoke(this, e.WasClean, e.Code, e.Reason);
if (!e.WasClean || e.Code != 1000)
Connect();
}
///
/// Event handler for WebSocketSharp's OnOpen event, handles gateway instructions and forwards to our events
///
/// Sender object
/// Event arguments
private void WebSocket_OnMessage(object sender, MessageEventArgs e)
{
// ignore non-text messages
if (!e.IsText)
return;
//Console.WriteLine(e.Data.Replace(client.Token, new string('*', client.Token.Length)));
OnSocketMessage?.Invoke(this, e.Data);
GatewayPayload payload = JsonConvert.DeserializeObject(e.Data);
switch (payload.OPCode) {
case GatewayOPCode.Dispatch:
LastSequence = payload.Sequence;
Enum.TryParse(payload.Name, out GatewayEvent evt);
switch (evt) {
#region Call
case GatewayEvent.CALL_CREATE:
OnCallCreate?.Invoke(this);
break;
case GatewayEvent.CALL_DELETE:
OnCallDelete?.Invoke(this);
break;
case GatewayEvent.CALL_UPDATE:
OnCallUpdate?.Invoke(this);
break;
case GatewayEvent.VOICE_SERVER_UPDATE:
OnVoiceServerUpdate?.Invoke(this);
break;
case GatewayEvent.VOICE_STATE_UPDATE:
OnVoiceStateUpdate?.Invoke(this);
break;
#endregion
#region Channel
case GatewayEvent.CHANNEL_CREATE:
OnChannelCreate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.CHANNEL_DELETE:
OnChannelDelete?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.CHANNEL_PINS_ACK:
OnChannelPinsAck?.Invoke(this);
break;
case GatewayEvent.CHANNEL_PINS_UPDATE:
OnChannelPinsUpdate?.Invoke(this);
break;
case GatewayEvent.CHANNEL_RECIPIENT_ADD:
OnChannelRecipientAdd?.Invoke(this);
break;
case GatewayEvent.CHANNEL_RECIPIENT_REMOVE:
OnChannelRecipientRemove?.Invoke(this);
break;
case GatewayEvent.CHANNEL_UPDATE:
OnChannelUpdate?.Invoke(this, payload.DataAs());
break;
#endregion
#region Guild
case GatewayEvent.GUILD_BAN_ADD:
OnGuildBanAdd?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_BAN_REMOVE:
OnGuildBanRemove?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_CREATE:
OnGuildCreate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_DELETE:
OnGuildDelete?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_EMOJIS_UPDATE:
OnGuildEmojisUpdate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_INTEGRATIONS_UPDATE:
OnGuildIntegrationsUpdate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_MEMBER_ADD:
OnGuildMemberAdd?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_MEMBER_UPDATE:
OnGuildMemberUpdate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_MEMBER_REMOVE:
OnGuildMemberRemove?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_MEMBERS_CHUNK:
OnGuildMembersChunk?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_ROLE_CREATE:
OnGuildRoleCreate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_ROLE_UPDATE:
OnGuildRoleUpdate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_ROLE_DELETE:
OnGuildRoleDelete?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.GUILD_UPDATE:
OnGuildUpdate?.Invoke(this, payload.DataAs());
break;
#endregion
#region Message
case GatewayEvent.MESSAGE_ACK:
OnMessageAck?.Invoke(this);
break;
case GatewayEvent.MESSAGE_CREATE:
OnMessageCreate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.MESSAGE_DELETE:
OnMessageDelete?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.MESSAGE_DELETE_BULK:
OnMessageDeleteBulk?.Invoke(this);
break;
case GatewayEvent.MESSAGE_REACTION_ADD:
OnMessageReactionAdd?.Invoke(this);
break;
case GatewayEvent.MESSAGE_REACTION_REMOVE:
OnMessageReactionRemove?.Invoke(this);
break;
case GatewayEvent.MESSAGE_REACTIONS_REMOVE_ALL:
OnMessageReactionsRemoveAll?.Invoke(this);
break;
case GatewayEvent.MESSAGE_UPDATE:
OnMessageUpdate?.Invoke(this, payload.DataAs());
break;
#endregion
#region Presence
case GatewayEvent.TYPING_START:
OnTypingStart?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.PRESENCE_UPDATE:
OnPresenceUpdate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.PRESENCES_REPLACE:
OnPresencesReplace?.Invoke(this);
break;
#endregion
#region Relations
case GatewayEvent.FRIEND_SUGGESTION_DELETE:
OnFriendSuggestionDelete?.Invoke(this);
break;
case GatewayEvent.RELATIONSHIP_ADD:
OnRelationshipAdd?.Invoke(this);
break;
case GatewayEvent.RELATIONSHIP_REMOVE:
OnRelationshipRemove?.Invoke(this);
break;
#endregion
#region State
case GatewayEvent.READY:
GatewayReady ready = payload.DataAs();
gatewayVersion = ready.Version;
session = ready.Session;
OnReady?.Invoke(this, ready);
break;
case GatewayEvent.RESUMED:
OnResumed?.Invoke(this);
break;
#endregion
#region User
case GatewayEvent.USER_UPDATE:
OnUserUpdate?.Invoke(this, payload.DataAs());
break;
case GatewayEvent.USER_NOTE_UPDATE:
OnUserNoteUpdate?.Invoke(this);
break;
case GatewayEvent.USER_SETTINGS_UPDATE:
OnUserSettingsUpdate?.Invoke(this);
break;
#endregion
default:
Console.WriteLine($"Unknown payload type: {payload.Name}");
break;
}
break;
case GatewayOPCode.Hello:
GatewayHello hello = payload.DataAs();
HeartbeatInterval = TimeSpan.FromMilliseconds(hello.HeartbeatInterval);
heartbeatHandler.Start();
if (string.IsNullOrEmpty(session))
Send(GatewayOPCode.Identify, new GatewayIdentification {
Token = client.Token,
Compress = false,
LargeThreshold = 250,
Shard = new int[2] { Id, client.ShardClient.ShardCount },
Properties = new GatewayIdentificationProperties {
OperatingSystem = @"windows",
Browser = @"Maki",
Device = @"Maki",
Referrer = string.Empty,
ReferringDomain = string.Empty,
}
});
else
Send(GatewayOPCode.Resume, new GatewayResume {
LastSequence = LastSequence ?? default(int),
Token = client.Token,
Session = session,
});
break;
}
}
#region IDisposable
private bool IsDisposed = false;
private void Dispose(bool disposing)
{
if (IsDisposed)
return;
IsDisposed = true;
Disconnect();
if (disposing)
GC.SuppressFinalize(this);
}
~GatewayShard()
=> Dispose(false);
public void Dispose()
=> Dispose(true);
#endregion
///
/// Connects to the gateway
///
public void Connect()
{
webSocket = new WebSocket($"{client.Gateway}?v={gatewayVersion}&encoding=json");
// make wss not log anything on its own
webSocket.Log.Output = (LogData logData, string path) => { };
webSocket.OnOpen += WebSocket_OnOpen;
webSocket.OnClose += WebSocket_OnClose;
webSocket.OnError += WebSocket_OnError;
webSocket.OnMessage += WebSocket_OnMessage;
webSocket.Connect();
}
///
/// Stops heartbeats and closes gateway connection for this shard
///
public void Disconnect()
{
heartbeatHandler.Stop();
if (webSocket.ReadyState != WebSocketState.Closed)
webSocket?.Close(CloseStatusCode.Normal);
}
///
/// Serialises and sends a json object
///
/// Type to serialise as
/// Data to serialise and send
public void Send(T data)
{
/*Console.WriteLine(
JsonConvert.SerializeObject(
data,
typeof(T),
new JsonSerializerSettings()
).Replace(client.Token, new string('*', client.Token.Length)));*/
webSocket.Send(
JsonConvert.SerializeObject(
data,
typeof(T),
new JsonSerializerSettings()
)
);
}
///
/// Serialises and sends a Gateway Payload
///
/// Opcode to use
/// Data to send
public void Send(GatewayOPCode opcode, object data)
{
Send(new GatewayPayload
{
OPCode = opcode,
Data = data
});
}
}
}