using PureWebSockets; using System; using System.Collections.Generic; using System.Net.WebSockets; using System.Threading; namespace SockChatKeepAlive { public sealed class SockChatClient : IDisposable { private readonly FutamiCommon Common; private readonly PersistentData Persist; private readonly Func GetAuthInfo; private PureWebSocket WebSocket; private Timer Pinger; private readonly object PersistAccess = new(); public record ChatUser(string Id, string Name, string Colour); private Dictionary Users { get; set; } = new(); public ChatUser MyUser { get; private set; } private bool IsDisposed = false; private DateTimeOffset LastPong = DateTimeOffset.Now; public SockChatClient( FutamiCommon common, PersistentData persist, Func getAuthInfo ) { Common = common; Persist = persist; GetAuthInfo = getAuthInfo; } ~SockChatClient() { DoDispose(); } public void Dispose() { DoDispose(); GC.SuppressFinalize(this); } private void DoDispose() { if(IsDisposed) return; IsDisposed = true; Disconnect(); } public void Connect() { string server = Common.Servers[RNG.Next(Common.Servers.Length)]; if(server.StartsWith("//")) server = "wss:" + server; Console.WriteLine($"Connecting to {server}..."); Disconnect(); WebSocket = new PureWebSocket(server, new PureWebSocketOptions { SubProtocols = new[] { "sockchat" }, }); WebSocket.OnOpened += WebSocket_OnOpen; WebSocket.OnClosed += WebSocket_OnClose; WebSocket.OnMessage += WebSocket_OnMessage; WebSocket.Connect(); } public void Disconnect() { MyUser = null; if(Pinger != null) { Pinger.Dispose(); Pinger = null; } if(WebSocket == null) return; try { WebSocket.OnOpened -= WebSocket_OnOpen; WebSocket.OnClosed -= WebSocket_OnClose; WebSocket.OnMessage -= WebSocket_OnMessage; WebSocket.Disconnect(); WebSocket = null; } catch { } } public void SendMessage(string text) { if(MyUser == null) return; Send("2", MyUser.Id, text.Replace("\t", " ")); } public void SendMessage(object obj) => SendMessage(obj.ToString()); public void SendPing() { if(MyUser != null) Send("0", MyUser.Id); } public void Send(params object[] args) { WebSocket.Send(string.Join("\t", args)); } private void WebSocket_OnOpen(object sender) { Console.WriteLine("WebSocket connected."); Console.WriteLine(); string[] authInfo = GetAuthInfo(); Send("1", authInfo[0] ?? string.Empty, authInfo[1] ?? string.Empty); } private void WebSocket_OnClose(object sender, WebSocketCloseStatus reason) { Console.WriteLine($"WebSocket disconnected: {reason}"); Disconnect(); if(!IsDisposed) { lock(PersistAccess) Persist.WasGracefulDisconnect = false; Thread.Sleep(10000); Connect(); } } private void WebSocket_OnMessage(object sender, string data) { string[] args = data.Split('\t'); if(args.Length < 1) return; switch(args[0]) { case "0": TimeSpan pongDiff = DateTimeOffset.Now - LastPong; LastPong = DateTimeOffset.Now; lock(PersistAccess) Persist.SatoriTotalUptime += (long)pongDiff.TotalMilliseconds; break; case "1": if(MyUser == null) { if(args[1] == "y") { Pinger = new Timer(x => SendPing(), null, 0, Common.Ping * 1000); } else { Disconnect(); return; } } Users[args[2]] = new(args[2], args[3], args[4]); if(MyUser == null) { MyUser = Users[args[2]]; lock(PersistAccess) { if(Persist.WasGracefulDisconnect) Persist.AFKString = MyUser.Name.StartsWith("<") ? MyUser.Name[4..MyUser.Name.IndexOf(">_")] : string.Empty; else { string afkString = Persist.AFKString; if(!string.IsNullOrWhiteSpace(afkString)) SendMessage($"/afk {afkString}"); } } } else Console.WriteLine($"!! {args[2]} <{args[3]}> joined."); break; case "2": Console.Write($":: {{{args[4]}}} [{DateTimeOffset.FromUnixTimeSeconds(long.Parse(args[1])):G}] "); if(Users.ContainsKey(args[2])) Console.Write($"{Users[args[2]].Id} <{Users[args[2]].Name}>"); else Console.Write("*"); Console.WriteLine(); foreach(string line in args[3].Split("
")) { Console.Write("> "); Console.WriteLine(line.Replace('\f', '\t').Trim()); } Console.WriteLine(); break; case "3": Users.Remove(args[1]); Console.WriteLine($"!! {args[1]} left."); break; case "5": switch(args[1]) { case "0": Users[args[2]] = new(args[2], args[3], args[4]); Console.WriteLine($"!! {args[2]} <{args[3]}> entered channel."); break; case "1": Users.Remove(args[2]); Console.WriteLine($"!! {args[2]} switched channel."); break; } break; case "6": Console.WriteLine($"!! Message {args[1]} was deleted."); break; case "7": if(args.Length < 2) break; switch(args[1]) { case "0": if(args.Length < 3 || !int.TryParse(args[2], out int amount)) break; int offset = 3; for(int i = 0; i < amount; i++) { Users[args[offset++]] = new(args[offset], args[offset++], args[offset++]); offset += 2; } break; case "1": Console.Write($":: {{{args[8]}}} [{DateTimeOffset.FromUnixTimeSeconds(long.Parse(args[2])):G}] "); if(args[3].Equals("-1", StringComparison.Ordinal)) Console.Write("*"); else Console.Write($"{args[3]} <{args[4]}>"); Console.WriteLine(); foreach(string line in args[7].Split("
")) { Console.Write("> "); Console.WriteLine(line.Replace('\f', '\t').Trim()); } Console.WriteLine(); break; } break; case "8": if(args[1] is "0" or "4") Console.WriteLine("!! Message list cleared"); if(args[1] is "1" or "3" or "4") { Console.WriteLine("!! User list cleared"); Users.Clear(); Users.Add(MyUser.Id, MyUser); } if(args[1] is "2" or "3" or "4") Console.WriteLine("!! Channel list cleared"); break; case "9": Console.WriteLine($"!! Kicked from server: {args[1]} {args[2]}"); break; case "10": Users[args[1]] = new(args[1], args[2], args[3]); Console.WriteLine($"!! {args[1]} updated: {args[2]}."); if(MyUser.Id == args[1]) { MyUser = Users[args[1]]; lock(PersistAccess) Persist.AFKString = MyUser.Name.StartsWith("<") ? MyUser.Name[4..MyUser.Name.IndexOf(">_")] : string.Empty; } break; } } } }