using Fleck; using SharpChat.Commands; using SharpChat.Config; using SharpChat.Events; using SharpChat.Misuzu; using SharpChat.Packet; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; namespace SharpChat { public class SockChatServer : IDisposable { public const ushort DEFAULT_PORT = 6770; public const int DEFAULT_MSG_LENGTH_MAX = 5000; public const int DEFAULT_MAX_CONNECTIONS = 5; public const int DEFAULT_FLOOD_KICK_LENGTH = 30; public bool IsDisposed { get; private set; } public static ChatUser Bot { get; } = new ChatUser { UserId = -1, Username = "ChatBot", Rank = 0, Colour = new ChatColour(), }; public IWebSocketServer Server { get; } public ChatContext Context { get; } private readonly HttpClient HttpClient; private readonly MisuzuClient Misuzu; private readonly CachedValue MaxMessageLength; private readonly CachedValue MaxConnections; private readonly CachedValue FloodKickLength; private IReadOnlyCollection Commands { get; } = new IChatCommand[] { new AFKCommand(), }; public List Sessions { get; } = new List(); private object SessionsLock { get; } = new object(); public ChatUserSession GetSession(IWebSocketConnection conn) { lock(SessionsLock) return Sessions.FirstOrDefault(x => x.Connection == conn); } private ManualResetEvent Shutdown { get; set; } private bool IsShuttingDown = false; public SockChatServer(HttpClient httpClient, MisuzuClient msz, IConfig config) { Logger.Write("Initialising Sock Chat server..."); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX); MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS); FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH); Context = new ChatContext(); string[] channelNames = config.ReadValue("channels", new[] { "lounge" }); foreach(string channelName in channelNames) { ChatChannel channelInfo = new(channelName); IConfig channelCfg = config.ScopeTo($"channels:{channelName}"); string tmp; tmp = channelCfg.SafeReadValue("name", string.Empty); if(!string.IsNullOrWhiteSpace(tmp)) channelInfo.Name = tmp; channelInfo.Password = channelCfg.SafeReadValue("password", string.Empty); channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0); Context.Channels.Add(channelInfo); } ushort port = config.SafeReadValue("port", DEFAULT_PORT); Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}"); } public void Listen(ManualResetEvent mre) { Shutdown = mre; Server.Start(sock => { if(IsShuttingDown || IsDisposed) { sock.Close(1013); return; } sock.OnOpen = () => OnOpen(sock); sock.OnClose = () => OnClose(sock); sock.OnError = err => OnError(sock, err); sock.OnMessage = msg => OnMessage(sock, msg); }); Logger.Write("Listening..."); } private void OnOpen(IWebSocketConnection conn) { lock(SessionsLock) { if(!Sessions.Any(x => x.Connection == conn)) Sessions.Add(new ChatUserSession(conn)); } Context.Update(); } private void OnClose(IWebSocketConnection conn) { ChatUserSession sess = GetSession(conn); // Remove connection from user if(sess?.User != null) { // RemoveConnection sets conn.User to null so we must grab a local copy. ChatUser user = sess.User; user.RemoveSession(sess); if(!user.HasSessions) Context.UserLeave(null, user); } // Update context Context.Update(); // Remove connection from server lock(SessionsLock) Sessions.Remove(sess); sess?.Dispose(); } private void OnError(IWebSocketConnection conn, Exception ex) { ChatUserSession sess = GetSession(conn); string sessId = sess?.Id ?? new string('0', ChatUserSession.ID_LENGTH); Logger.Write($"[{sessId} {conn.ConnectionInfo.ClientIpAddress}] {ex}"); Context.Update(); } private readonly object BumpAccess = new(); private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1); private DateTimeOffset LastBump = DateTimeOffset.MinValue; private void OnMessage(IWebSocketConnection conn, string msg) { Context.Update(); ChatUserSession sess = GetSession(conn); if(sess == null) { conn.Close(); return; } if(sess.User is not null && sess.User.HasFloodProtection) { sess.User.RateLimiter.AddTimePoint(); if(sess.User.RateLimiter.State == ChatRateLimitState.Kick) { Task.Run(async () => { TimeSpan duration = TimeSpan.FromSeconds(FloodKickLength); await Misuzu.CreateBanAsync( sess.User.UserId.ToString(), sess.RemoteAddress.ToString(), string.Empty, "::1", duration, "Kicked from chat for flood protection." ); Context.BanUser(sess.User, duration, UserDisconnectReason.Flood); }).Wait(); return; } else if(sess.User.RateLimiter.State == ChatRateLimitState.Warning) sess.User.Send(new FloodWarningPacket()); } string[] args = msg.Split('\t'); if(args.Length < 1) return; switch(args[0]) { case "0": if(!int.TryParse(args[1], out int pTime)) break; sess.BumpPing(); sess.Send(new PongPacket(sess.LastPing)); lock(BumpAccess) { if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { (string, string)[] bumpList = Context.Users .Where(u => u.HasSessions && u.Status == ChatUserStatus.Online) .Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty)) .ToArray(); if(bumpList.Any()) Task.Run(async () => { await Misuzu.BumpUsersOnlineAsync(bumpList); }).Wait(); LastBump = DateTimeOffset.UtcNow; } } break; case "1": if(sess.User != null) break; string authMethod = args.ElementAtOrDefault(1); if(string.IsNullOrWhiteSpace(authMethod)) { sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); break; } string authToken = args.ElementAtOrDefault(2); if(string.IsNullOrWhiteSpace(authToken)) { sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); break; } if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) { string[] tokenParts = authToken.Split(':', 2); authMethod = tokenParts[0]; authToken = tokenParts[1]; } Task.Run(async () => { MisuzuAuthInfo fai; string ipAddr = sess.RemoteAddress.ToString(); try { fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr); } catch(Exception ex) { Logger.Write($"<{sess.Id}> Failed to authenticate: {ex}"); sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); #if DEBUG throw; #else return; #endif } if(!fai.Success) { Logger.Debug($"<{sess.Id}> Auth fail: {fai.Reason}"); sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); return; } MisuzuBanInfo fbi; try { fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr); } catch(Exception ex) { Logger.Write($"<{sess.Id}> Failed auth ban check: {ex}"); sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); #if DEBUG throw; #else return; #endif } if(fbi.IsBanned && !fbi.HasExpired) { Logger.Write($"<{sess.Id}> User is banned."); sess.Send(new AuthFailPacket(AuthFailReason.Banned, fbi)); sess.Dispose(); return; } ChatUser aUser = Context.Users.Get(fai.UserId); if(aUser == null) aUser = new ChatUser(fai); else { aUser.ApplyAuth(fai); aUser.Channel?.Send(new UserUpdatePacket(aUser)); } // Enforce a maximum amount of connections per user if(aUser.SessionCount >= MaxConnections) { sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); sess.Dispose(); return; } // Bumping the ping to prevent upgrading sess.BumpPing(); aUser.AddSession(sess); sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, $"Welcome to Flashii Chat, {aUser.Username}!")); if(File.Exists("welcome.txt")) { IEnumerable lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); string line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); if(!string.IsNullOrWhiteSpace(line)) sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); } Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess, MaxMessageLength); }).Wait(); break; case "2": if(args.Length < 3) break; ChatUser mUser = sess.User; // No longer concats everything after index 1 with \t, no previous implementation did that either string messageText = args.ElementAtOrDefault(2); if(mUser == null || !mUser.Can(ChatUserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText)) break; #if !DEBUG // Extra validation step, not necessary at all but enforces proper formatting in SCv1. if (!long.TryParse(args[1], out long mUserId) || mUser.UserId != mUserId) break; #endif ChatChannel mChannel = mUser.CurrentChannel; if(mChannel == null || !mUser.InChannel(mChannel) || (mUser.IsSilenced && !mUser.Can(ChatUserPermissions.SilenceUser))) break; if(mUser.Status != ChatUserStatus.Online) { mUser.Status = ChatUserStatus.Online; mChannel.Send(new UserUpdatePacket(mUser)); } int maxMsgLength = MaxMessageLength; if(messageText.Length > maxMsgLength) messageText = messageText[..maxMsgLength]; messageText = messageText.Trim(); #if DEBUG Logger.Write($"<{sess.Id} {mUser.Username}> {messageText}"); #endif IChatMessage message = null; if(messageText[0] == '/') { message = HandleV1Command(messageText, mUser, mChannel, sess); if(message == null) break; } message ??= new ChatMessage { Target = mChannel, TargetName = mChannel.TargetName, DateTime = DateTimeOffset.UtcNow, Sender = mUser, Text = messageText, }; Context.Events.Add(message); mChannel.Send(new ChatMessageAddPacket(message)); break; } } public IChatMessage HandleV1Command(string message, ChatUser user, ChatChannel channel, ChatUserSession sess) { string[] parts = message[1..].Split(' '); string commandName = parts[0].Replace(".", string.Empty).ToLowerInvariant(); for(int i = 1; i < parts.Length; i++) parts[i] = parts[i].Replace("<", "<") .Replace(">", ">") .Replace("\n", "
"); IChatCommand command = null; foreach(IChatCommand cmd in Commands) if(cmd.IsMatch(commandName)) { command = cmd; break; } if(command != null) return command.Dispatch(new ChatCommandContext(parts, user, channel)); switch(commandName) { case "nick": // sets a temporary nickname bool setOthersNick = user.Can(ChatUserPermissions.SetOthersNickname); if(!setOthersNick && !user.Can(ChatUserPermissions.SetOwnNickname)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } ChatUser targetUser = null; int offset = 1; if(setOthersNick && parts.Length > 1 && long.TryParse(parts[1], out long targetUserId) && targetUserId > 0) { targetUser = Context.Users.Get(targetUserId); offset = 2; } targetUser ??= user; if(parts.Length < offset) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } string nickStr = string.Join('_', parts.Skip(offset)) .Replace(' ', '_') .Replace("\n", string.Empty) .Replace("\r", string.Empty) .Replace("\f", string.Empty) .Replace("\t", string.Empty) .Trim(); if(nickStr == targetUser.Username) nickStr = null; else if(nickStr.Length > 15) nickStr = nickStr[..15]; else if(string.IsNullOrEmpty(nickStr)) nickStr = null; if(nickStr != null && Context.Users.Get(nickStr) != null) { user.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); break; } string previousName = targetUser == user ? (targetUser.Nickname ?? targetUser.Username) : null; targetUser.Nickname = nickStr; channel.Send(new UserUpdatePacket(targetUser, previousName)); break; case "whisper": // sends a pm to another user case "msg": if(parts.Length < 3) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } ChatUser whisperUser = Context.Users.Get(parts[1]); if(whisperUser == null) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts[1])); break; } if(whisperUser == user) break; string whisperStr = string.Join(' ', parts.Skip(2)); whisperUser.Send(new ChatMessageAddPacket(new ChatMessage { DateTime = DateTimeOffset.Now, Target = whisperUser, TargetName = whisperUser.TargetName, Sender = user, Text = whisperStr, Flags = ChatMessageFlags.Private, })); user.Send(new ChatMessageAddPacket(new ChatMessage { DateTime = DateTimeOffset.Now, Target = whisperUser, TargetName = whisperUser.TargetName, Sender = user, Text = $"{whisperUser.DisplayName} {whisperStr}", Flags = ChatMessageFlags.Private, })); break; case "action": // describe an action case "me": if(parts.Length < 2) break; string actionMsg = string.Join(' ', parts.Skip(1)); return new ChatMessage { Target = channel, TargetName = channel.TargetName, DateTime = DateTimeOffset.UtcNow, Sender = user, Text = actionMsg, Flags = ChatMessageFlags.Action, }; case "who": // gets all online users/online users in a channel if arg StringBuilder whoChanSB = new(); string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty; if(!string.IsNullOrEmpty(whoChanStr)) { ChatChannel whoChan = Context.Channels.Get(whoChanStr); if(whoChan == null) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); break; } if(whoChan.Rank > user.Rank || (whoChan.HasPassword && !user.Can(ChatUserPermissions.JoinAnyChannel))) { user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_ERROR, true, whoChanStr)); break; } foreach(ChatUser whoUser in whoChan.GetUsers()) { whoChanSB.Append(@"'); whoChanSB.Append(whoUser.DisplayName); whoChanSB.Append(", "); } if(whoChanSB.Length > 2) whoChanSB.Length -= 2; user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB)); } else { foreach(ChatUser whoUser in Context.Users.All()) { whoChanSB.Append(@"'); whoChanSB.Append(whoUser.DisplayName); whoChanSB.Append(", "); } if(whoChanSB.Length > 2) whoChanSB.Length -= 2; user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_SERVER, false, whoChanSB)); } break; // double alias for delchan and delmsg // if the argument is a number we're deleting a message // if the argument is a string we're deleting a channel case "delete": if(parts.Length < 2) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } if(parts[1].All(char.IsDigit)) goto case "delmsg"; goto case "delchan"; // anyone can use these case "join": // join a channel if(parts.Length < 2) break; ChatChannel joinChan = Context.Channels.Get(parts[1]); if(joinChan == null) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, parts[1])); user.ForceChannel(); break; } Context.SwitchChannel(user, joinChan, string.Join(' ', parts.Skip(2))); break; case "create": // create a new channel if(user.Can(ChatUserPermissions.CreateChannel)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } bool createChanHasHierarchy; if(parts.Length < 2 || (createChanHasHierarchy = parts[1].All(char.IsDigit) && parts.Length < 3)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } int createChanHierarchy = 0; if(createChanHasHierarchy) if(!int.TryParse(parts[1], out createChanHierarchy)) createChanHierarchy = 0; if(createChanHierarchy > user.Rank) { user.Send(new LegacyCommandResponse(LCR.INSUFFICIENT_HIERARCHY)); break; } string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1)); ChatChannel createChan = new() { Name = createChanName, IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent), Rank = createChanHierarchy, Owner = user, }; try { Context.Channels.Add(createChan); } catch(ChannelExistException) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChan.Name)); break; } catch(ChannelInvalidNameException) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID)); break; } Context.SwitchChannel(user, createChan, createChan.Password); user.Send(new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name)); break; case "delchan": // delete a channel if(parts.Length < 2 || string.IsNullOrWhiteSpace(parts[1])) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } string delChanName = string.Join('_', parts.Skip(1)); ChatChannel delChan = Context.Channels.Get(delChanName); if(delChan == null) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName)); break; } if(!user.Can(ChatUserPermissions.DeleteChannel) && delChan.Owner != user) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETE_FAILED, true, delChan.Name)); break; } Context.Channels.Remove(delChan); user.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name)); break; case "password": // set a password on the channel case "pwd": if(!user.Can(ChatUserPermissions.SetChannelPassword) || channel.Owner != user) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } string chanPass = string.Join(' ', parts.Skip(1)).Trim(); if(string.IsNullOrWhiteSpace(chanPass)) chanPass = string.Empty; Context.Channels.Update(channel, password: chanPass); user.Send(new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false)); break; case "privilege": // sets a minimum hierarchy requirement on the channel case "rank": case "priv": if(!user.Can(ChatUserPermissions.SetChannelHierarchy) || channel.Owner != user) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } if(parts.Length < 2 || !int.TryParse(parts[1], out int chanHierarchy) || chanHierarchy > user.Rank) { user.Send(new LegacyCommandResponse(LCR.INSUFFICIENT_HIERARCHY)); break; } Context.Channels.Update(channel, hierarchy: chanHierarchy); user.Send(new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false)); break; case "say": // pretend to be the bot if(!user.Can(ChatUserPermissions.Broadcast)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } Context.Send(new LegacyCommandResponse(LCR.BROADCAST, false, string.Join(' ', parts.Skip(1)))); break; case "delmsg": // deletes a message bool deleteAnyMessage = user.Can(ChatUserPermissions.DeleteAnyMessage); if(!deleteAnyMessage && !user.Can(ChatUserPermissions.DeleteOwnMessage)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } if(parts.Length < 2 || !parts[1].All(char.IsDigit) || !long.TryParse(parts[1], out long delSeqId)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } IChatEvent delMsg = Context.Events.Get(delSeqId); if(delMsg == null || delMsg.Sender.Rank > user.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != user.UserId)) { user.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR)); break; } Context.Events.Remove(delMsg); break; case "kick": // kick a user from the server case "ban": // ban a user from the server, this differs from /kick in that it adds all remote address to the ip banlist bool isBanning = commandName == "ban"; if(!user.Can(isBanning ? ChatUserPermissions.BanUser : ChatUserPermissions.KickUser)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } string banUserTarget = parts.ElementAtOrDefault(1); string banDurationStr = parts.ElementAtOrDefault(2); int banReasonIndex = 2; ChatUser banUser = null; if(banUserTarget == null || (banUser = Context.Users.Get(banUserTarget)) == null) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); break; } if(banUser == user || banUser.Rank >= user.Rank) { user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); break; } TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero; if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) { if(durationSeconds < 0) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } duration = TimeSpan.FromSeconds(durationSeconds); ++banReasonIndex; } if(duration <= TimeSpan.Zero) { Context.BanUser(banUser, duration); break; } string banReason = string.Join(' ', parts.Skip(banReasonIndex)); Task.Run(async () => { // obviously it makes no sense to only check for one ip address but that's current misuzu limitations MisuzuBanInfo fbi = await Misuzu.CheckBanAsync( banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString() ); if(fbi.IsBanned && !fbi.HasExpired) { user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); return; } await Misuzu.CreateBanAsync( banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString(), user.UserId.ToString(), sess.RemoteAddress.ToString(), duration, banReason ); Context.BanUser(banUser, duration); }).Wait(); break; case "pardon": case "unban": if(!user.Can(ChatUserPermissions.BanUser | ChatUserPermissions.KickUser)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } bool unbanUserTargetIsName = true; string unbanUserTarget = parts.ElementAtOrDefault(1); if(string.IsNullOrWhiteSpace(unbanUserTarget)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } ChatUser unbanUser = Context.Users.Get(unbanUserTarget); if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { unbanUserTargetIsName = false; unbanUser = Context.Users.Get(unbanUserId); } if(unbanUser != null) unbanUserTarget = unbanUser.UserId.ToString(); Task.Run(async () => { MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName); if(!banInfo.IsBanned || banInfo.HasExpired) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget)); return; } bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId); if(wasBanned) user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUserTarget)); else user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget)); }).Wait(); break; case "pardonip": case "unbanip": if(!user.Can(ChatUserPermissions.BanUser | ChatUserPermissions.KickUser)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } string unbanAddrTarget = parts.ElementAtOrDefault(1); if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress unbanAddr)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } unbanAddrTarget = unbanAddr.ToString(); Task.Run(async () => { MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget); if(!banInfo.IsBanned || banInfo.HasExpired) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget)); return; } bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress); if(wasBanned) user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanAddrTarget)); else user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget)); }).Wait(); break; case "bans": // gets a list of bans case "banned": if(!user.Can(ChatUserPermissions.BanUser | ChatUserPermissions.KickUser)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } Task.Run(async () => { user.Send(new BanListPacket( await Misuzu.GetBanListAsync() )); }).Wait(); break; case "silence": // silence a user if(!user.Can(ChatUserPermissions.SilenceUser)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } ChatUser silUser; if(parts.Length < 2 || (silUser = Context.Users.Get(parts[1])) == null) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); break; } if(silUser == user) { user.Send(new LegacyCommandResponse(LCR.SILENCE_SELF)); break; } if(silUser.Rank >= user.Rank) { user.Send(new LegacyCommandResponse(LCR.SILENCE_HIERARCHY)); break; } if(silUser.IsSilenced) { user.Send(new LegacyCommandResponse(LCR.SILENCE_ALREADY)); break; } DateTimeOffset silenceUntil = DateTimeOffset.MaxValue; if(parts.Length > 2) { if(!double.TryParse(parts[2], out double silenceSeconds)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } silenceUntil = DateTimeOffset.UtcNow.AddSeconds(silenceSeconds); } silUser.SilencedUntil = silenceUntil; silUser.Send(new LegacyCommandResponse(LCR.SILENCED, false)); user.Send(new LegacyCommandResponse(LCR.TARGET_SILENCED, false, silUser.DisplayName)); break; case "unsilence": // unsilence a user if(!user.Can(ChatUserPermissions.SilenceUser)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } ChatUser unsilUser; if(parts.Length < 2 || (unsilUser = Context.Users.Get(parts[1])) == null) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); break; } if(unsilUser.Rank >= user.Rank) { user.Send(new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY)); break; } if(!unsilUser.IsSilenced) { user.Send(new LegacyCommandResponse(LCR.NOT_SILENCED)); break; } unsilUser.SilencedUntil = DateTimeOffset.MinValue; unsilUser.Send(new LegacyCommandResponse(LCR.UNSILENCED, false)); user.Send(new LegacyCommandResponse(LCR.TARGET_UNSILENCED, false, unsilUser.DisplayName)); break; case "ip": // gets a user's ip (from all connections in this case) case "whois": if(!user.Can(ChatUserPermissions.SeeIPAddress)) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, "/ip")); break; } ChatUser ipUser; if(parts.Length < 2 || (ipUser = Context.Users.Get(parts[1])) == null) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); break; } foreach(IPAddress ip in ipUser.RemoteAddresses.Distinct().ToArray()) user.Send(new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip)); break; case "shutdown": case "restart": if(user.UserId != 1) { user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{commandName}")); break; } if(IsShuttingDown) break; IsShuttingDown = true; if(commandName == "restart") lock(SessionsLock) Sessions.ForEach(s => s.PrepareForRestart()); Context.Update(); Shutdown?.Set(); break; default: user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_FOUND, true, commandName)); break; } return null; } ~SockChatServer() { DoDispose(); } public void Dispose() { DoDispose(); GC.SuppressFinalize(this); } private void DoDispose() { if(IsDisposed) return; IsDisposed = true; lock(SessionsLock) Sessions.ForEach(s => s.Dispose()); Server?.Dispose(); Context?.Dispose(); HttpClient?.Dispose(); } } }