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) { IsDisposed = true; Disconnect(); } } ~GatewayShard() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(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 }); } } }