diff --git a/RustPlusApi/Examples/Fcm/FcmListener/Program.cs b/RustPlusApi/Examples/Fcm/FcmListener/Program.cs index 65b2880..da03dcb 100644 --- a/RustPlusApi/Examples/Fcm/FcmListener/Program.cs +++ b/RustPlusApi/Examples/Fcm/FcmListener/Program.cs @@ -1,27 +1,25 @@ -using Newtonsoft.Json; - -using RustPlusApi.Fcm; +using RustPlusApi.Fcm; using RustPlusApi.Fcm.Data; var credentials = new Credentials { Keys = new Keys { - PrivateKey = "PNo-juMIoy8nh45ap7CcOjmvCcXG71zxo1Kf6sG75yI", - PublicKey = "BMLRwfGJ3poc2ih6eIQpf-7xwkP9z98K8vh-bWzxypDERTUIyAqpulccHR6WBVP8jgoecNtYePTYvSc-sHGhWcY", - AuthSecret = "3_stBv0R105hCQJgJ_Oqag", + PrivateKey = "", + PublicKey = "", + AuthSecret = "", }, Fcm = new FcmCredentials { - Token = "de0gThSZW5A:APA91bEpz3S-tIVNV4uoKKNc_UCA_8tcg5lxEWiXkx-zFb0-H6FG5ltQvkGYzfxcfm3GxNihFgfRvH7Ps0_IdvRpOvjVREsm8uJwhkt8ztaoOZ886osG-bvGGVfV2jt1rPDG1_22NoMm", - PushSet = "dEhc-sLR9LY", + Token = "", + PushSet = "", }, Gcm = new GcmCredentials { - Token = "ctMfS0V2BB0:APA91bG5LKmV_pe27v7Drm4OBYMkZS8eItKHUX-wG5eUw1WmKFTSkwYr4hw43AQmP7oBwHzeKbMRDEmaml0lh2CBvh0s3pXxrDSng6au3t-iE1FkEhpBOwaDh74Hk2z6Y8GcFTcaPsA2", - AndroidId = 5198350057269518718, - SecurityToken = 726893779159876265, - AppId = "wp:receiver.push.com#46616054-1728-4be7-9dc6-740f3e76d9f4", + Token = "", + AndroidId = 0, + SecurityToken = 0, + AppId = "", } }; @@ -29,7 +27,7 @@ listener.NotificationReceived += (_, message) => { - Console.WriteLine($"{DateTime.Now}:\n{message}"); + Console.WriteLine($"[MESSAGE]: {DateTime.Now}:\n{message}"); }; listener.ErrorOccurred += (_, error) => diff --git a/RustPlusApi/RustPlusApi.Fcm/Data/Events/AlarmEventArg.cs b/RustPlusApi/RustPlusApi.Fcm/Data/Events/AlarmEventArg.cs new file mode 100644 index 0000000..445ffe7 --- /dev/null +++ b/RustPlusApi/RustPlusApi.Fcm/Data/Events/AlarmEventArg.cs @@ -0,0 +1,9 @@ +namespace RustPlusApi.Fcm.Data.Events +{ + public class AlarmEventArg + { + public Guid ServerId { get; set; } + public string Title { get; set; } = null!; + public string Message { get; set; } = null!; + } +} diff --git a/RustPlusApi/RustPlusApi.Fcm/Data/Events/EntityEventArg.cs b/RustPlusApi/RustPlusApi.Fcm/Data/Events/EntityEventArg.cs new file mode 100644 index 0000000..509000b --- /dev/null +++ b/RustPlusApi/RustPlusApi.Fcm/Data/Events/EntityEventArg.cs @@ -0,0 +1,9 @@ +namespace RustPlusApi.Fcm.Data.Events +{ + public class EntityEventArg + { + public int EntityType { get; set; } + public int EntityId { get; set; } + public string EntityName { get; set; } = null!; + } +} diff --git a/RustPlusApi/RustPlusApi.Fcm/Utils/MessageEventArg.cs b/RustPlusApi/RustPlusApi.Fcm/Data/Events/MessageEventArg.cs similarity index 84% rename from RustPlusApi/RustPlusApi.Fcm/Utils/MessageEventArg.cs rename to RustPlusApi/RustPlusApi.Fcm/Data/Events/MessageEventArg.cs index 305775a..34a64d7 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Utils/MessageEventArg.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Data/Events/MessageEventArg.cs @@ -1,6 +1,6 @@ using static RustPlusApi.Fcm.Data.Constants; -namespace RustPlusApi.Fcm.Utils +namespace RustPlusApi.Fcm.Data.Events { internal class MessageEventArgs : EventArgs { diff --git a/RustPlusApi/RustPlusApi.Fcm/Data/Events/ServerEventArg.cs b/RustPlusApi/RustPlusApi.Fcm/Data/Events/ServerEventArg.cs new file mode 100644 index 0000000..8e53c3d --- /dev/null +++ b/RustPlusApi/RustPlusApi.Fcm/Data/Events/ServerEventArg.cs @@ -0,0 +1,9 @@ + +namespace RustPlusApi.Fcm.Data.Events +{ + public class ServerEventArg + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + } +} diff --git a/RustPlusApi/RustPlusApi.Fcm/Data/Events/ServerFullEventArg.cs b/RustPlusApi/RustPlusApi.Fcm/Data/Events/ServerFullEventArg.cs new file mode 100644 index 0000000..f6a9cf5 --- /dev/null +++ b/RustPlusApi/RustPlusApi.Fcm/Data/Events/ServerFullEventArg.cs @@ -0,0 +1,12 @@ +namespace RustPlusApi.Fcm.Data.Events +{ + public class ServerFullEventArg : ServerEventArg + { + public string Ip { get; set; } = null!; + public int Port { get; set; } + public string Desc { get; set; } = null!; + public int Logo { get; set; } + public int Img { get; set; } + public string Url { get; set; } = null!; + } +} diff --git a/RustPlusApi/RustPlusApi.Fcm/Data/FcmMessage.cs b/RustPlusApi/RustPlusApi.Fcm/Data/FcmMessage.cs index 77d0447..aaa2cf0 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Data/FcmMessage.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Data/FcmMessage.cs @@ -6,39 +6,39 @@ namespace RustPlusApi.Fcm.Data { public class FcmMessage { - public Data Data { get; set; } = null!; - public string From { get; set; } = null!; + public Guid FcmMessageId { get; set; } public string Priority { get; set; } = null!; - public string FcmMessageId { get; set; } = null!; + public long From { get; set; } + public MessageData Data { get; set; } = null!; } - public class Data + public class MessageData { + public Guid ProjectId { get; set; } + public string ChannelId { get; set; } = null!; + public string Title { get; set; } = null!; + public string Message { get; set; } = null!; public string ExperienceId { get; set; } = null!; public string ScopeKey { get; set; } = null!; [JsonConverter(typeof(BodyConverter))] public Body Body { get; set; } = null!; - public string Message { get; set; } = null!; - public string Title { get; set; } = null!; - public string ProjectId { get; set; } = null!; - public string ChannelId { get; set; } = null!; } public class Body { - public string Img { get; set; } = null!; - public string EntityType { get; set; } = null!; + public Guid Id { get; set; } public string Ip { get; set; } = null!; - public string EntityId { get; set; } = null!; - public string Type { get; set; } = null!; + public int Port { get; set; } + public string Name { get; set; } = null!; + public string Desc { get; set; } = null!; + public int Logo { get; set; } + public int Img { get; set; } public string Url { get; set; } = null!; + public ulong PlayerId { get; set; } public string PlayerToken { get; set; } = null!; - public string Port { get; set; } = null!; + public string Type { get; set; } = null!; + public int? EntityType { get; set; } + public int? EntityId { get; set; } public string EntityName { get; set; } = null!; - public string Name { get; set; } = null!; - public string Logo { get; set; } = null!; - public string Id { get; set; } = null!; - public string Desc { get; set; } = null!; - public string PlayerId { get; set; } = null!; } } diff --git a/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs b/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs index 07ea1f0..ccea4e9 100644 --- a/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs +++ b/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs @@ -1,300 +1,110 @@ using System.Diagnostics; -using System.Net.Security; -using System.Net.Sockets; -using System.Numerics; - -using McsProto; - -using Newtonsoft.Json; - -using ProtoBuf; using RustPlusApi.Fcm.Data; -using RustPlusApi.Fcm.Utils; - -using static RustPlusApi.Fcm.Data.Constants; -using static System.GC; +using RustPlusApi.Fcm.Data.Events; namespace RustPlusApi.Fcm { - public class FcmListener(Credentials credentials, ICollection? persistentIds = null) : IDisposable + public class FcmListener(Credentials credentials, ICollection? persistentIds = null) : FcmListenerBasic(credentials, persistentIds) { - private const string Host = "mtalk.google.com"; - private const int Port = 5228; - - private TcpClient? _tcpClient; - private SslStream? _sslStream; - private DateTime _lastReset; - private DateTime _timeLastMessageReceived; - private Timer? _checkinTimer; - - public event EventHandler? Connecting; - public event EventHandler? Connected; - public event EventHandler? NotificationReceived; - public event EventHandler? Disconnected; - public event EventHandler? ErrorOccurred; - - public async Task ConnectAsync() - { - _tcpClient = new TcpClient(); - await _tcpClient.ConnectAsync(Host, Port); - - _sslStream = new SslStream(_tcpClient.GetStream(), false); - await _sslStream.AuthenticateAsClientAsync(Host); - - Connecting?.Invoke(this, EventArgs.Empty); - - try - { - var loginRequest = new LoginRequest - { - AdaptiveHeartbeat = false, - auth_service = LoginRequest.AuthService.AndroidId, - AuthToken = credentials.Gcm.SecurityToken.ToString(), - Id = "chrome-63.0.3234.0", - Domain = "mcs.android.com", - DeviceId = $"android-{BigInteger.Parse(credentials.Gcm.AndroidId.ToString()):X}", - NetworkType = 1, - Resource = credentials.Gcm.AndroidId.ToString(), - User = credentials.Gcm.AndroidId.ToString(), - UseRmq2 = true, - Settings = { new Setting() { Name = "new_vc", Value = "1" } }, - ClientEvents = { new ClientEvent() }, - ReceivedPersistentIds = { }, - }; + public event EventHandler? OnParing; - if (persistentIds != null) loginRequest.ReceivedPersistentIds.AddRange(persistentIds); + public event EventHandler<(ServerEventArg, EntityEventArg)>? OnEntityParing; + public event EventHandler? OnServerPairing; - SendPacket(loginRequest); + public event EventHandler<(ServerEventArg, int)>? OnSmartSwitchParing; + public event EventHandler<(ServerEventArg, int)>? OnSmartAlarmParing; + public event EventHandler<(ServerEventArg, int)>? OnStorageMonitorParing; - _lastReset = DateTime.Now; - _timeLastMessageReceived = DateTime.Now; - - Connected?.Invoke(this, EventArgs.Empty); - - StatusCheck(); - ReceiveMessages(); - } - catch (Exception ex) - { - ErrorOccurred?.Invoke(this, ex); - Dispose(); - } - } - - public void Dispose() - { - _sslStream?.Dispose(); - _tcpClient?.Dispose(); - - Disconnected?.Invoke(this, EventArgs.Empty); - - SuppressFinalize(this); - } + public event EventHandler? OnAlarmTriggered; - private void ReceiveMessages() + protected override void ParseNotification(FcmMessage? message) { - var parser = new Parser(); - parser.MessageReceived += (_, e) => OnMessage(e); + if (message == null) return; - // First receival (LoginResponse) - byte[] header = Read(2); - int version = header[0]; - int tag = header[1]; - - if (version < KMcsVersion && version != 38) - throw new InvalidOperationException($"Protocol version {version} unsupported"); - - int size = ReadVarint32(); - Debug.WriteLine($"Got message size: {size} bytes"); - - byte[] payload = Read(size); - Debug.WriteLine($"Successfully read: {payload.Length} bytes"); - - Type type = Parser.BuildProtobufFromTag(((McsProtoTag)tag)); - Debug.WriteLine($"RECEIVED PROTO OF TYPE {type.Name}"); - - if (type != typeof(LoginResponse)) - throw new Exception($"Got wrong login response. Expected {typeof(LoginResponse).Name}, got {type.Name}"); - - parser.OnGotLoginResponse(); - parser.OnData(payload, type); - - // Start receival of the rest of messages - Debug.WriteLine("Starting receiver loop."); - while (true) - { - tag = _sslStream!.ReadByte(); - size = ReadVarint32(); - payload = Read(size); - type = Parser.BuildProtobufFromTag((McsProtoTag)tag); - Debug.WriteLine($"RECEIVED PROTO OF TYPE {type.Name}"); - - parser.OnData(payload, type); - } - } - - internal byte[] Read(int size) - { - byte[] buffer = new byte[size]; - int bytesRead = 0; - while (bytesRead < size) + switch (message.Data.ChannelId) { - bytesRead += _sslStream!.Read(buffer, bytesRead, size - bytesRead); - } - return buffer; - } - - internal int ReadVarint32() - { - int result = 0; - int shift = 0; - while (true) - { - byte b = (byte)_sslStream!.ReadByte(); - result |= (b & 0x7F) << shift; - if ((b & 0x80) == 0) break; - shift += 7; + case "pairing": + OnParing?.Invoke(this, message.Data); + ParsePairing(message.Data.Body); + break; + case "alarm": + var alarm = new AlarmEventArg() + { + ServerId = message.Data.Body.Id, + Title = message.Data.Title, + Message = message.Data.Message + }; + OnAlarmTriggered?.Invoke(this, alarm); + break; + default: + Debug.WriteLine($"Unknown channel: {message.Data.ChannelId}"); + break; } - return result; } - internal static byte[] EncodeVarint32(int value) + private void ParsePairing(Body body) { - List result = []; - while (value != 0) + switch (body.Type) { - byte b = (byte)(value & 0x7F); - value >>= 7; - if (value != 0) b |= 0x80; - result.Add(b); + case "entity": + var server = new ServerEventArg() + { + Id = body.Id, + Name = body.Name + }; + var entity = new EntityEventArg() + { + EntityType = body.EntityType ?? 0, + EntityId = body.EntityId ?? 0, + EntityName = body.EntityName + }; + OnEntityParing?.Invoke(this, (server, entity)); + ParsePairingEntity(body); + break; + case "server": + var serverFull = new ServerFullEventArg() + { + Id = body.Id, + Name = body.Name, + Ip = body.Ip, + Port = body.Port, + Desc = body.Desc, + Logo = body.Logo, + Img = body.Img, + Url = body.Url + }; + OnServerPairing?.Invoke(this, serverFull); + break; + default: + Debug.WriteLine($"Unknown pairing type: {body.Type}"); + break; } - return [.. result]; - } - - private void SendPacket(object packet) - { - var tagEnum = Parser.GetTagFromProtobufType(packet.GetType()); - var header = new byte[] { KMcsVersion, (byte)(int)tagEnum }; - - using var ms = new MemoryStream(); - Serializer.Serialize(ms, packet); - - byte[] payload = ms.ToArray(); - _sslStream!.Write([.. header, .. EncodeVarint32(payload.Length), .. payload]); } - private void HandlePing(HeartbeatPing? ping) + private void ParsePairingEntity(Body body) { - if (ping == null) return; - - Debug.WriteLine($"Responding to ping: Stream ID: {ping.StreamId}, Last: {ping.LastStreamIdReceived}, Status: {ping.Status}"); - var pingResponse = new HeartbeatAck + var server = new ServerEventArg() { - StreamId = ping.StreamId + 1, - LastStreamIdReceived = ping.StreamId, - Status = ping.Status + Id = body.Id, + Name = body.Name }; - SendPacket(pingResponse); - } - - private void Reset(bool noWait = false) - { - if (!noWait) - { - var timeSinceLastReset = DateTime.Now - _lastReset; - - if (timeSinceLastReset < TimeSpan.FromSeconds(MinResetIntervalSecs)) - { - Debug.WriteLine($"{timeSinceLastReset.TotalSeconds}s since last reset attempt."); - - var waitTime = TimeSpan.FromSeconds(MinResetIntervalSecs) - timeSinceLastReset; - - Debug.WriteLine($"Waiting {waitTime.TotalSeconds}seconds"); - Thread.Sleep(waitTime); - } - } - _lastReset = DateTime.Now; - - Debug.WriteLine("Resetting listener."); - Dispose(); - - ConnectAsync().GetAwaiter().GetResult(); - } - - private void StatusCheck(object? state = null) - { - TimeSpan timeSinceLastMessage = DateTime.UtcNow - _timeLastMessageReceived; - if (timeSinceLastMessage > TimeSpan.FromSeconds(MaxSilentIntervalSecs)) + switch (body.EntityType) { - Debug.WriteLine($"No communications received in {timeSinceLastMessage.TotalSeconds}s. Resetting connection."); - Reset(true); - } - else - { - int expectedTimeout = 1 + MaxSilentIntervalSecs - (int)timeSinceLastMessage.TotalSeconds; - _checkinTimer = new Timer(StatusCheck, null, expectedTimeout * 1000, Timeout.Infinite); - } - } - - private void OnMessage(MessageEventArgs e) - { - _timeLastMessageReceived = DateTime.Now; - - // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - switch (e.Tag) - { - case McsProtoTag.KLoginResponseTag: - persistentIds?.Clear(); - break; - case McsProtoTag.KDataMessageStanzaTag: - OnDataMessage(e.Object as DataMessageStanza); + case 1: + OnSmartSwitchParing?.Invoke(this, (server, body.EntityId ?? 0)); break; - case McsProtoTag.KHeartbeatPingTag: - HandlePing(e.Object as HeartbeatPing); + case 2: + OnSmartAlarmParing?.Invoke(this, (server, body.EntityId ?? 0)); break; - case McsProtoTag.KCloseTag: - Reset(true); + case 3: + OnStorageMonitorParing?.Invoke(this, (server, body.EntityId ?? 0)); break; - case McsProtoTag.KIqStanzaTag: - break; // I'm not sure about what this message does, and it arrives partially empty, so I will just leave it like this for now default: - throw new ArgumentOutOfRangeException($"Unrecognized tag: {e.Tag}"); - } - } - - protected void OnDataMessage(DataMessageStanza? dataMessage) - { - if (dataMessage?.PersistentId != null - && persistentIds != null - && persistentIds!.Contains(dataMessage?.PersistentId!)) - return; - - var message = string.Empty; - try - { - message = DecryptionUtility.Decrypt(dataMessage!, credentials.Keys); - } - catch (Exception ex) - { - if (ex.Message.Contains("Unsupported state or unable to authenticate data") || - ex.Message.Contains("crypto-key is missing") || - ex.Message.Contains("salt is missing")) - { - Debug.WriteLine($"Message dropped as it could not be decrypted: {ex.Message}"); - return; - } - } - finally - { - persistentIds?.Add(dataMessage!.PersistentId); + Debug.WriteLine($"Unknown entity type: {body.EntityType}"); + break; } - - var fcmMessage = JsonConvert.DeserializeObject(message); - var betterMessage = JsonConvert.SerializeObject(fcmMessage, Formatting.Indented); - - NotificationReceived?.Invoke(this, betterMessage); } } } diff --git a/RustPlusApi/RustPlusApi.Fcm/FcmListenerBasic.cs b/RustPlusApi/RustPlusApi.Fcm/FcmListenerBasic.cs new file mode 100644 index 0000000..9f28df7 --- /dev/null +++ b/RustPlusApi/RustPlusApi.Fcm/FcmListenerBasic.cs @@ -0,0 +1,307 @@ +using System.Diagnostics; +using System.Net.Security; +using System.Net.Sockets; +using System.Numerics; + +using McsProto; + +using Newtonsoft.Json; + +using ProtoBuf; + +using RustPlusApi.Fcm.Data; +using RustPlusApi.Fcm.Utils; + +using static RustPlusApi.Fcm.Data.Constants; +using static System.GC; +using RustPlusApi.Fcm.Data.Events; + +namespace RustPlusApi.Fcm +{ + public class FcmListenerBasic(Credentials credentials, ICollection? persistentIds = null) : IDisposable + { + private const string Host = "mtalk.google.com"; + private const int Port = 5228; + + private TcpClient? _tcpClient; + private SslStream? _sslStream; + private DateTime _lastReset; + private DateTime _timeLastMessageReceived; + private Timer? _checkinTimer; + + public event EventHandler? Connecting; + public event EventHandler? Connected; + public event EventHandler? NotificationReceived; + public event EventHandler? Disconnected; + public event EventHandler? ErrorOccurred; + + public async Task ConnectAsync() + { + _tcpClient = new TcpClient(); + await _tcpClient.ConnectAsync(Host, Port); + + _sslStream = new SslStream(_tcpClient.GetStream(), false); + await _sslStream.AuthenticateAsClientAsync(Host); + + Connecting?.Invoke(this, EventArgs.Empty); + + try + { + var loginRequest = new LoginRequest + { + AdaptiveHeartbeat = false, + auth_service = LoginRequest.AuthService.AndroidId, + AuthToken = credentials.Gcm.SecurityToken.ToString(), + Id = "chrome-63.0.3234.0", + Domain = "mcs.android.com", + DeviceId = $"android-{BigInteger.Parse(credentials.Gcm.AndroidId.ToString()):X}", + NetworkType = 1, + Resource = credentials.Gcm.AndroidId.ToString(), + User = credentials.Gcm.AndroidId.ToString(), + UseRmq2 = true, + Settings = { new Setting() { Name = "new_vc", Value = "1" } }, + ClientEvents = { new ClientEvent() }, + ReceivedPersistentIds = { }, + }; + + if (persistentIds != null) loginRequest.ReceivedPersistentIds.AddRange(persistentIds); + + SendPacket(loginRequest); + + _lastReset = DateTime.Now; + _timeLastMessageReceived = DateTime.Now; + + Connected?.Invoke(this, EventArgs.Empty); + + StatusCheck(); + ReceiveMessages(); + } + catch (Exception ex) + { + ErrorOccurred?.Invoke(this, ex); + Dispose(); + } + } + + public void Dispose() + { + _sslStream?.Dispose(); + _tcpClient?.Dispose(); + + Disconnected?.Invoke(this, EventArgs.Empty); + + SuppressFinalize(this); + } + + private void ReceiveMessages() + { + var parser = new RawMessageParser(); + parser.MessageReceived += (_, e) => OnMessage(e); + + // First receival (LoginResponse) + byte[] header = Read(2); + int version = header[0]; + int tag = header[1]; + + if (version < KMcsVersion && version != 38) + throw new InvalidOperationException($"Protocol version {version} unsupported"); + + int size = ReadVarint32(); + Debug.WriteLine($"Got message size: {size} bytes"); + + byte[] payload = Read(size); + Debug.WriteLine($"Successfully read: {payload.Length} bytes"); + + Type type = RawMessageParser.BuildProtobufFromTag(((McsProtoTag)tag)); + Debug.WriteLine($"RECEIVED PROTO OF TYPE {type.Name}"); + + if (type != typeof(LoginResponse)) + throw new Exception($"Got wrong login response. Expected {typeof(LoginResponse).Name}, got {type.Name}"); + + parser.OnGotLoginResponse(); + parser.OnData(payload, type); + + // Start receival of the rest of messages + Debug.WriteLine("Starting receiver loop."); + while (true) + { + tag = _sslStream!.ReadByte(); + size = ReadVarint32(); + payload = Read(size); + type = RawMessageParser.BuildProtobufFromTag((McsProtoTag)tag); + Debug.WriteLine($"RECEIVED PROTO OF TYPE {type.Name}"); + + parser.OnData(payload, type); + } + } + + private byte[] Read(int size) + { + byte[] buffer = new byte[size]; + int bytesRead = 0; + while (bytesRead < size) + { + bytesRead += _sslStream!.Read(buffer, bytesRead, size - bytesRead); + } + return buffer; + } + + private int ReadVarint32() + { + int result = 0; + int shift = 0; + while (true) + { + byte b = (byte)_sslStream!.ReadByte(); + result |= (b & 0x7F) << shift; + if ((b & 0x80) == 0) break; + shift += 7; + } + return result; + } + + private static byte[] EncodeVarint32(int value) + { + List result = []; + while (value != 0) + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if (value != 0) b |= 0x80; + result.Add(b); + } + return [.. result]; + } + + private void SendPacket(object packet) + { + var tagEnum = RawMessageParser.GetTagFromProtobufType(packet.GetType()); + var header = new byte[] { KMcsVersion, (byte)(int)tagEnum }; + + using var ms = new MemoryStream(); + Serializer.Serialize(ms, packet); + + byte[] payload = ms.ToArray(); + _sslStream!.Write([.. header, .. EncodeVarint32(payload.Length), .. payload]); + } + + private void HandlePing(HeartbeatPing? ping) + { + if (ping == null) return; + + Debug.WriteLine($"Responding to ping: Stream ID: {ping.StreamId}, Last: {ping.LastStreamIdReceived}, Status: {ping.Status}"); + var pingResponse = new HeartbeatAck + { + StreamId = ping.StreamId + 1, + LastStreamIdReceived = ping.StreamId, + Status = ping.Status + }; + + SendPacket(pingResponse); + } + + private void Reset(bool noWait = false) + { + if (!noWait) + { + var timeSinceLastReset = DateTime.Now - _lastReset; + + if (timeSinceLastReset < TimeSpan.FromSeconds(MinResetIntervalSecs)) + { + Debug.WriteLine($"{timeSinceLastReset.TotalSeconds}s since last reset attempt."); + + var waitTime = TimeSpan.FromSeconds(MinResetIntervalSecs) - timeSinceLastReset; + + Debug.WriteLine($"Waiting {waitTime.TotalSeconds}seconds"); + Thread.Sleep(waitTime); + } + } + _lastReset = DateTime.Now; + + Debug.WriteLine("Resetting listener."); + Dispose(); + + ConnectAsync().GetAwaiter().GetResult(); + } + + private void StatusCheck(object? state = null) + { + TimeSpan timeSinceLastMessage = DateTime.UtcNow - _timeLastMessageReceived; + if (timeSinceLastMessage > TimeSpan.FromSeconds(MaxSilentIntervalSecs)) + { + Debug.WriteLine($"No communications received in {timeSinceLastMessage.TotalSeconds}s. Resetting connection."); + Reset(true); + } + else + { + int expectedTimeout = 1 + MaxSilentIntervalSecs - (int)timeSinceLastMessage.TotalSeconds; + _checkinTimer = new Timer(StatusCheck, null, expectedTimeout * 1000, Timeout.Infinite); + } + } + + private void OnMessage(MessageEventArgs e) + { + _timeLastMessageReceived = DateTime.Now; + + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + switch (e.Tag) + { + case McsProtoTag.KLoginResponseTag: + persistentIds?.Clear(); + break; + case McsProtoTag.KDataMessageStanzaTag: + OnDataMessage(e.Object as DataMessageStanza); + break; + case McsProtoTag.KHeartbeatPingTag: + HandlePing(e.Object as HeartbeatPing); + break; + case McsProtoTag.KCloseTag: + Reset(true); + break; + case McsProtoTag.KIqStanzaTag: + break; // I'm not sure about what this message does, and it arrives partially empty, so I will just leave it like this for now + default: + throw new ArgumentOutOfRangeException($"Unrecognized tag: {e.Tag}"); + } + } + + private void OnDataMessage(DataMessageStanza? dataMessage) + { + if (dataMessage?.PersistentId != null + && persistentIds != null + && persistentIds!.Contains(dataMessage?.PersistentId!)) + return; + + var message = string.Empty; + try + { + message = DecryptionUtility.Decrypt(dataMessage!, credentials.Keys); + } + catch (Exception ex) + { + if (ex.Message.Contains("Unsupported state or unable to authenticate data") || + ex.Message.Contains("crypto-key is missing") || + ex.Message.Contains("salt is missing")) + { + Debug.WriteLine($"Message dropped as it could not be decrypted: {ex.Message}"); + return; + } + } + finally + { + persistentIds?.Add(dataMessage!.PersistentId); + } + + var fcmMessage = JsonConvert.DeserializeObject(message); + + fcmMessage!.Data.Body.Desc = ""; + + var betterMessage = JsonConvert.SerializeObject(fcmMessage, Formatting.Indented); + + ParseNotification(fcmMessage); + NotificationReceived?.Invoke(this, betterMessage); + } + + protected virtual void ParseNotification(FcmMessage? message) { } + } +} diff --git a/RustPlusApi/RustPlusApi.Fcm/Utils/Parser.cs b/RustPlusApi/RustPlusApi.Fcm/Utils/RawMessageParser.cs similarity index 98% rename from RustPlusApi/RustPlusApi.Fcm/Utils/Parser.cs rename to RustPlusApi/RustPlusApi.Fcm/Utils/RawMessageParser.cs index c81e127..cd7b541 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Utils/Parser.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Utils/RawMessageParser.cs @@ -4,11 +4,13 @@ using ProtoBuf; +using RustPlusApi.Fcm.Data.Events; + using static RustPlusApi.Fcm.Data.Constants; namespace RustPlusApi.Fcm.Utils { - internal class Parser() + internal class RawMessageParser() { internal event EventHandler? ErrorOccurred; internal event EventHandler? MessageReceived;