diff --git a/SharpChat/ChatCommandContext.cs b/SharpChat/ChatCommandContext.cs new file mode 100644 index 0000000..fb0fa0f --- /dev/null +++ b/SharpChat/ChatCommandContext.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; + +namespace SharpChat { + public class ChatCommandContext { + public string Name { get; } + public string[] Args { get; } + public ChatContext Chat { get; } + public ChatUser User { get; } + public ChatUserSession Session { get; } + public ChatChannel Channel { get; } + + public ChatCommandContext( + string text, + ChatContext chat, + ChatUser user, + ChatUserSession session, + ChatChannel channel + ) { + if(text == null) + throw new ArgumentNullException(nameof(text)); + + Chat = chat ?? throw new ArgumentNullException(nameof(chat)); + User = user ?? throw new ArgumentNullException(nameof(user)); + Session = session ?? throw new ArgumentNullException(nameof(session)); + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + + string[] parts = text[1..].Split(' '); + Name = parts.First().Replace(".", string.Empty); + Args = parts.Skip(1).ToArray(); + } + + public ChatCommandContext( + string name, + string[] args, + ChatContext chat, + ChatUser user, + ChatUserSession session, + ChatChannel channel + ) { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Args = args ?? throw new ArgumentNullException(nameof(args)); + Chat = chat ?? throw new ArgumentNullException(nameof(chat)); + User = user ?? throw new ArgumentNullException(nameof(user)); + Session = session ?? throw new ArgumentNullException(nameof(session)); + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + } + + public bool NameEquals(string name) { + return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase); + } + } +} diff --git a/SharpChat/ChatContext.cs b/SharpChat/ChatContext.cs index 3f60ef6..6929c42 100644 --- a/SharpChat/ChatContext.cs +++ b/SharpChat/ChatContext.cs @@ -1,4 +1,5 @@ -using SharpChat.Events; +using Fleck; +using SharpChat.Events; using SharpChat.EventStorage; using SharpChat.Packet; using System; @@ -10,6 +11,9 @@ namespace SharpChat { public HashSet Channels { get; } = new(); public readonly object ChannelsAccess = new(); + public HashSet Sessions { get; } = new(); + public readonly object SessionsAccess = new(); + public HashSet Users { get; } = new(); public readonly object UsersAccess = new(); @@ -36,6 +40,10 @@ namespace SharpChat { } } + public ChatUserSession GetSession(IWebSocketConnection conn) { + return Sessions.FirstOrDefault(s => s.Connection == conn); + } + public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) { if(duration > TimeSpan.Zero) user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration)); diff --git a/SharpChat/ChatUserSession.cs b/SharpChat/ChatUserSession.cs index d5ae3bd..d4724f4 100644 --- a/SharpChat/ChatUserSession.cs +++ b/SharpChat/ChatUserSession.cs @@ -83,5 +83,9 @@ namespace SharpChat { IsDisposed = true; Connection.Close(CloseCode); } + + public override int GetHashCode() { + return Id.GetHashCode(); + } } } diff --git a/SharpChat/Commands/AFKCommand.cs b/SharpChat/Commands/AFKCommand.cs index 41e2d4c..67ff58d 100644 --- a/SharpChat/Commands/AFKCommand.cs +++ b/SharpChat/Commands/AFKCommand.cs @@ -1,5 +1,4 @@ -using SharpChat.Events; -using SharpChat.Packet; +using SharpChat.Packet; using System.Linq; namespace SharpChat.Commands { @@ -7,12 +6,12 @@ namespace SharpChat.Commands { private const string DEFAULT = "AFK"; private const int MAX_LENGTH = 5; - public bool IsMatch(string name) { - return name == "afk"; + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("afk"); } - public IChatMessage Dispatch(IChatCommandContext context) { - string statusText = context.Args.ElementAtOrDefault(1); + public void Dispatch(ChatCommandContext ctx) { + string statusText = ctx.Args.FirstOrDefault(); if(string.IsNullOrWhiteSpace(statusText)) statusText = DEFAULT; else { @@ -21,11 +20,9 @@ namespace SharpChat.Commands { statusText = statusText[..MAX_LENGTH].Trim(); } - context.User.Status = ChatUserStatus.Away; - context.User.StatusMessage = statusText; - context.Channel.Send(new UserUpdatePacket(context.User)); - - return null; + ctx.User.Status = ChatUserStatus.Away; + ctx.User.StatusMessage = statusText; + ctx.Channel.Send(new UserUpdatePacket(ctx.User)); } } } diff --git a/SharpChat/Commands/ActionCommand.cs b/SharpChat/Commands/ActionCommand.cs new file mode 100644 index 0000000..5a56f88 --- /dev/null +++ b/SharpChat/Commands/ActionCommand.cs @@ -0,0 +1,30 @@ +using SharpChat.Events; +using System; +using System.Linq; + +namespace SharpChat.Commands { + public class ActionCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("action") + || ctx.NameEquals("me"); + } + + public void Dispatch(ChatCommandContext ctx) { + throw new NotImplementedException(); + } + + public ChatMessage ActionDispatch(ChatCommandContext ctx) { + if(!ctx.Args.Any()) + return null; + + return new ChatMessage { + Target = ctx.Channel, + TargetName = ctx.Channel.TargetName, + DateTime = DateTimeOffset.UtcNow, + Sender = ctx.User, + Text = string.Join(' ', ctx.Args), + Flags = ChatMessageFlags.Action, + }; + } + } +} diff --git a/SharpChat/Commands/BanListCommand.cs b/SharpChat/Commands/BanListCommand.cs new file mode 100644 index 0000000..6371356 --- /dev/null +++ b/SharpChat/Commands/BanListCommand.cs @@ -0,0 +1,32 @@ +using SharpChat.Misuzu; +using SharpChat.Packet; +using System; +using System.Threading.Tasks; + +namespace SharpChat.Commands { + public class BanListCommand : IChatCommand { + private readonly MisuzuClient Misuzu; + + public BanListCommand(MisuzuClient msz) { + Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); + } + + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("bans") + || ctx.NameEquals("banned"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.BanUser | ChatUserPermissions.KickUser)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + Task.Run(async () => { + ctx.User.Send(new BanListPacket( + await Misuzu.GetBanListAsync() + )); + }).Wait(); + } + } +} diff --git a/SharpChat/Commands/BroadcastCommand.cs b/SharpChat/Commands/BroadcastCommand.cs new file mode 100644 index 0000000..37fc090 --- /dev/null +++ b/SharpChat/Commands/BroadcastCommand.cs @@ -0,0 +1,19 @@ +using SharpChat.Packet; + +namespace SharpChat.Commands { + public class BroadcastCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("say") + || ctx.NameEquals("broadcast"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.Broadcast)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + ctx.Chat.Send(new LegacyCommandResponse(LCR.BROADCAST, false, string.Join(' ', ctx.Args))); + } + } +} diff --git a/SharpChat/Commands/CreateChannelCommand.cs b/SharpChat/Commands/CreateChannelCommand.cs new file mode 100644 index 0000000..6a3a0c0 --- /dev/null +++ b/SharpChat/Commands/CreateChannelCommand.cs @@ -0,0 +1,65 @@ +using SharpChat.Packet; +using System.Linq; + +namespace SharpChat.Commands { + public class CreateChannelCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("create"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(ctx.User.Can(ChatUserPermissions.CreateChannel)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + string firstArg = ctx.Args.First(); + + bool createChanHasHierarchy; + if(!ctx.Args.Any() || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + int createChanHierarchy = 0; + if(createChanHasHierarchy) + if(!int.TryParse(firstArg, out createChanHierarchy)) + createChanHierarchy = 0; + + if(createChanHierarchy > ctx.User.Rank) { + ctx.User.Send(new LegacyCommandResponse(LCR.INSUFFICIENT_HIERARCHY)); + return; + } + + string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0)); + + if(!ChatChannel.CheckName(createChanName)) { + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID)); + return; + } + + lock(ctx.Chat.ChannelsAccess) { + if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) { + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); + return; + } + + ChatChannel createChan = new() { + Name = createChanName, + IsTemporary = !ctx.User.Can(ChatUserPermissions.SetChannelPermanent), + Rank = createChanHierarchy, + Owner = ctx.User, + }; + + ctx.Chat.Channels.Add(createChan); + lock(ctx.Chat.UsersAccess) { + foreach(ChatUser ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank)) + ccu.Send(new ChannelCreatePacket(ctx.Channel)); + } + + ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password); + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name)); + } + } + } +} diff --git a/SharpChat/Commands/DeleteChannelCommand.cs b/SharpChat/Commands/DeleteChannelCommand.cs new file mode 100644 index 0000000..925f159 --- /dev/null +++ b/SharpChat/Commands/DeleteChannelCommand.cs @@ -0,0 +1,39 @@ +using SharpChat.Packet; +using System.Linq; + +namespace SharpChat.Commands { + public class DeleteChannelCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("delchan") || ( + ctx.NameEquals("delete") + && ctx.Args.FirstOrDefault()?.All(char.IsDigit) == false + ); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.Args.Any() || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + string delChanName = string.Join('_', ctx.Args); + ChatChannel delChan; + lock(ctx.Chat.ChannelsAccess) + delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName)); + + if(delChan == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName)); + return; + } + + if(!ctx.User.Can(ChatUserPermissions.DeleteChannel) && delChan.Owner != ctx.User) { + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETE_FAILED, true, delChan.Name)); + return; + } + + lock(ctx.Chat.ChannelsAccess) + ctx.Chat.RemoveChannel(delChan); + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name)); + } + } +} diff --git a/SharpChat/Commands/DeleteMessageCommand.cs b/SharpChat/Commands/DeleteMessageCommand.cs new file mode 100644 index 0000000..d97c2b2 --- /dev/null +++ b/SharpChat/Commands/DeleteMessageCommand.cs @@ -0,0 +1,42 @@ +using SharpChat.Events; +using SharpChat.Packet; +using System.Linq; + +namespace SharpChat.Commands { + public class DeleteMessageCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("delmsg") || ( + ctx.NameEquals("delete") + && ctx.Args.FirstOrDefault()?.All(char.IsDigit) == true + ); + } + + public void Dispatch(ChatCommandContext ctx) { + bool deleteAnyMessage = ctx.User.Can(ChatUserPermissions.DeleteAnyMessage); + + if(!deleteAnyMessage && !ctx.User.Can(ChatUserPermissions.DeleteOwnMessage)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + string firstArg = ctx.Args.FirstOrDefault(); + + if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long delSeqId)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + lock(ctx.Chat.EventsAccess) { + IChatEvent delMsg = ctx.Chat.Events.GetEvent(delSeqId); + + if(delMsg == null || delMsg.Sender.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != ctx.User.UserId)) { + ctx.User.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR)); + return; + } + + ctx.Chat.Events.RemoveEvent(delMsg); + ctx.Chat.Send(new ChatMessageDeletePacket(delMsg.SequenceId)); + } + } + } +} diff --git a/SharpChat/Commands/JoinChannelCommand.cs b/SharpChat/Commands/JoinChannelCommand.cs new file mode 100644 index 0000000..edf4623 --- /dev/null +++ b/SharpChat/Commands/JoinChannelCommand.cs @@ -0,0 +1,25 @@ +using SharpChat.Packet; +using System.Linq; + +namespace SharpChat.Commands { + public class JoinChannelCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("join"); + } + + public void Dispatch(ChatCommandContext ctx) { + string joinChanStr = ctx.Args.FirstOrDefault(); + ChatChannel joinChan; + lock(ctx.Chat.ChannelsAccess) + joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); + + if(joinChan == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); + ctx.User.ForceChannel(); + return; + } + + ctx.Chat.SwitchChannel(ctx.User, joinChan, string.Join(' ', ctx.Args.Skip(1))); + } + } +} diff --git a/SharpChat/Commands/KickBanCommand.cs b/SharpChat/Commands/KickBanCommand.cs new file mode 100644 index 0000000..1b7708a --- /dev/null +++ b/SharpChat/Commands/KickBanCommand.cs @@ -0,0 +1,83 @@ +using SharpChat.Misuzu; +using SharpChat.Packet; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace SharpChat.Commands { + public class KickBanCommand : IChatCommand { + private readonly MisuzuClient Misuzu; + + public KickBanCommand(MisuzuClient msz) { + Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); + } + + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("kick") + || ctx.NameEquals("ban"); + } + + public void Dispatch(ChatCommandContext ctx) { + bool isBanning = ctx.NameEquals("ban"); + + if(!ctx.User.Can(isBanning ? ChatUserPermissions.BanUser : ChatUserPermissions.KickUser)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + string banUserTarget = ctx.Args.ElementAtOrDefault(0); + string banDurationStr = ctx.Args.ElementAtOrDefault(1); + int banReasonIndex = 1; + ChatUser banUser = null; + + lock(ctx.Chat.UsersAccess) + if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); + return; + } + + if(banUser == ctx.User || banUser.Rank >= ctx.User.Rank) { + ctx.User.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); + return; + } + + TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero; + if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) { + if(durationSeconds < 0) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + duration = TimeSpan.FromSeconds(durationSeconds); + ++banReasonIndex; + } + + if(duration <= TimeSpan.Zero) { + ctx.Chat.BanUser(banUser, duration); + return; + } + + string banReason = string.Join(' ', ctx.Args.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) { + ctx.User.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); + return; + } + + await Misuzu.CreateBanAsync( + banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString(), + ctx.User.UserId.ToString(), ctx.Session.RemoteAddress.ToString(), + duration, banReason + ); + + ctx.Chat.BanUser(banUser, duration); + }).Wait(); + } + } +} diff --git a/SharpChat/Commands/NickCommand.cs b/SharpChat/Commands/NickCommand.cs new file mode 100644 index 0000000..e8f7eb1 --- /dev/null +++ b/SharpChat/Commands/NickCommand.cs @@ -0,0 +1,56 @@ +using SharpChat.Packet; +using System.Linq; + +namespace SharpChat.Commands { + public class NickCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("nick"); + } + + public void Dispatch(ChatCommandContext ctx) { + bool setOthersNick = ctx.User.Can(ChatUserPermissions.SetOthersNickname); + + if(!setOthersNick && !ctx.User.Can(ChatUserPermissions.SetOwnNickname)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + ChatUser targetUser = null; + int offset = 0; + + if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) { + lock(ctx.Chat.UsersAccess) + targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId); + ++offset; + } + + targetUser ??= ctx.User; + + if(ctx.Args.Length < offset) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + string nickStr = string.Join('_', ctx.Args.Skip(offset)) + .Replace("\n", string.Empty).Replace("\r", string.Empty) + .Replace("\f", string.Empty).Replace("\t", string.Empty) + .Replace(' ', '_').Trim(); + + if(nickStr == targetUser.Username) + nickStr = null; + else if(nickStr.Length > 15) + nickStr = nickStr[..15]; + else if(string.IsNullOrEmpty(nickStr)) + nickStr = null; + + lock(ctx.Chat.UsersAccess) + if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) { + ctx.User.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); + return; + } + + string previousName = targetUser == ctx.User ? (targetUser.Nickname ?? targetUser.Username) : null; + targetUser.Nickname = nickStr; + ctx.Channel.Send(new UserUpdatePacket(targetUser, previousName)); + } + } +} diff --git a/SharpChat/Commands/PardonAddressCommand.cs b/SharpChat/Commands/PardonAddressCommand.cs new file mode 100644 index 0000000..6a218df --- /dev/null +++ b/SharpChat/Commands/PardonAddressCommand.cs @@ -0,0 +1,51 @@ +using SharpChat.Misuzu; +using SharpChat.Packet; +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace SharpChat.Commands { + public class PardonAddressCommand : IChatCommand { + private readonly MisuzuClient Misuzu; + + public PardonAddressCommand(MisuzuClient msz) { + Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); + } + + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("pardonip") + || ctx.NameEquals("unbanip"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.BanUser | ChatUserPermissions.KickUser)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + string unbanAddrTarget = ctx.Args.FirstOrDefault(); + if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress unbanAddr)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + unbanAddrTarget = unbanAddr.ToString(); + + Task.Run(async () => { + MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget); + + if(!banInfo.IsBanned || banInfo.HasExpired) { + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget)); + return; + } + + bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress); + if(wasBanned) + ctx.User.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanAddrTarget)); + else + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget)); + }).Wait(); + } + } +} diff --git a/SharpChat/Commands/PardonUserCommand.cs b/SharpChat/Commands/PardonUserCommand.cs new file mode 100644 index 0000000..5b691a0 --- /dev/null +++ b/SharpChat/Commands/PardonUserCommand.cs @@ -0,0 +1,61 @@ +using SharpChat.Misuzu; +using SharpChat.Packet; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace SharpChat.Commands { + public class PardonUserCommand : IChatCommand { + private readonly MisuzuClient Misuzu; + + public PardonUserCommand(MisuzuClient msz) { + Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); + } + + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("pardon") + || ctx.NameEquals("unban"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.BanUser | ChatUserPermissions.KickUser)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + bool unbanUserTargetIsName = true; + string unbanUserTarget = ctx.Args.FirstOrDefault(); + if(string.IsNullOrWhiteSpace(unbanUserTarget)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + ChatUser unbanUser; + lock(ctx.Chat.UsersAccess) + unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); + if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { + unbanUserTargetIsName = false; + lock(ctx.Chat.UsersAccess) + unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId); + } + + if(unbanUser != null) + unbanUserTarget = unbanUser.UserId.ToString(); + + Task.Run(async () => { + MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName); + + if(!banInfo.IsBanned || banInfo.HasExpired) { + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget)); + return; + } + + bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId); + if(wasBanned) + ctx.User.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUserTarget)); + else + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget)); + }).Wait(); + } + } +} diff --git a/SharpChat/Commands/PasswordChannelCommand.cs b/SharpChat/Commands/PasswordChannelCommand.cs new file mode 100644 index 0000000..e28122f --- /dev/null +++ b/SharpChat/Commands/PasswordChannelCommand.cs @@ -0,0 +1,26 @@ +using SharpChat.Packet; + +namespace SharpChat.Commands { + public class PasswordChannelCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("pwd") + || ctx.NameEquals("password"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.SetChannelPassword) || ctx.Channel.Owner != ctx.User) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + string chanPass = string.Join(' ', ctx.Args).Trim(); + + if(string.IsNullOrWhiteSpace(chanPass)) + chanPass = string.Empty; + + lock(ctx.Chat.ChannelsAccess) + ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass); + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false)); + } + } +} diff --git a/SharpChat/Commands/RankChannelCommand.cs b/SharpChat/Commands/RankChannelCommand.cs new file mode 100644 index 0000000..77c03e5 --- /dev/null +++ b/SharpChat/Commands/RankChannelCommand.cs @@ -0,0 +1,28 @@ +using SharpChat.Packet; +using System.Linq; + +namespace SharpChat.Commands { + public class RankChannelCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("rank") + || ctx.NameEquals("privilege") + || ctx.NameEquals("priv"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.SetChannelHierarchy) || ctx.Channel.Owner != ctx.User) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + if(!ctx.Args.Any() || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) { + ctx.User.Send(new LegacyCommandResponse(LCR.INSUFFICIENT_HIERARCHY)); + return; + } + + lock(ctx.Chat.ChannelsAccess) + ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy); + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false)); + } + } +} diff --git a/SharpChat/Commands/RemoteAddressCommand.cs b/SharpChat/Commands/RemoteAddressCommand.cs new file mode 100644 index 0000000..ce59faa --- /dev/null +++ b/SharpChat/Commands/RemoteAddressCommand.cs @@ -0,0 +1,31 @@ +using SharpChat.Packet; +using System.Linq; +using System.Net; + +namespace SharpChat.Commands { + public class RemoteAddressCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("ip") + || ctx.NameEquals("whois"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.SeeIPAddress)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, "/ip")); + return; + } + + string ipUserStr = ctx.Args.FirstOrDefault(); + ChatUser ipUser; + + lock(ctx.Chat.UsersAccess) + if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, ipUserStr ?? "User")); + return; + } + + foreach(IPAddress ip in ipUser.RemoteAddresses.Distinct().ToArray()) + ctx.User.Send(new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip)); + } + } +} diff --git a/SharpChat/Commands/ShutdownRestartCommand.cs b/SharpChat/Commands/ShutdownRestartCommand.cs new file mode 100644 index 0000000..fb43b71 --- /dev/null +++ b/SharpChat/Commands/ShutdownRestartCommand.cs @@ -0,0 +1,38 @@ +using SharpChat.Packet; +using System; +using System.Threading; + +namespace SharpChat.Commands { + public class ShutdownRestartCommand : IChatCommand { + private readonly ManualResetEvent WaitHandle; + private readonly Func ShutdownCheck; + + public ShutdownRestartCommand(ManualResetEvent waitHandle, Func shutdownCheck) { + WaitHandle = waitHandle ?? throw new ArgumentNullException(nameof(waitHandle)); + ShutdownCheck = shutdownCheck ?? throw new ArgumentNullException(nameof(shutdownCheck)); + } + + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("shutdown") + || ctx.NameEquals("restart"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(ctx.User.UserId != 1) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + if(!ShutdownCheck()) + return; + + if(ctx.NameEquals("restart")) + lock(ctx.Chat.SessionsAccess) + foreach(ChatUserSession sess in ctx.Chat.Sessions) + sess.PrepareForRestart(); + + ctx.Chat.Update(); + WaitHandle?.Set(); + } + } +} diff --git a/SharpChat/Commands/SilenceApplyCommand.cs b/SharpChat/Commands/SilenceApplyCommand.cs new file mode 100644 index 0000000..9585fa4 --- /dev/null +++ b/SharpChat/Commands/SilenceApplyCommand.cs @@ -0,0 +1,57 @@ +using SharpChat.Packet; +using System; +using System.Linq; + +namespace SharpChat.Commands { + public class SilenceApplyCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("silence"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.SilenceUser)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + string silUserStr = ctx.Args.FirstOrDefault(); + ChatUser silUser; + + lock(ctx.Chat.UsersAccess) + if(string.IsNullOrWhiteSpace(silUserStr) || (silUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, silUserStr ?? "User")); + return; + } + + if(silUser == ctx.User) { + ctx.User.Send(new LegacyCommandResponse(LCR.SILENCE_SELF)); + return; + } + + if(silUser.Rank >= ctx.User.Rank) { + ctx.User.Send(new LegacyCommandResponse(LCR.SILENCE_HIERARCHY)); + return; + } + + if(silUser.IsSilenced) { + ctx.User.Send(new LegacyCommandResponse(LCR.SILENCE_ALREADY)); + return; + } + + DateTimeOffset silenceUntil = DateTimeOffset.MaxValue; + + if(ctx.Args.Length > 1) { + if(!double.TryParse(ctx.Args.ElementAt(1), out double silenceSeconds)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + silenceUntil = DateTimeOffset.UtcNow.AddSeconds(silenceSeconds); + } + + silUser.SilencedUntil = silenceUntil; + silUser.Send(new LegacyCommandResponse(LCR.SILENCED, false)); + ctx.User.Send(new LegacyCommandResponse(LCR.TARGET_SILENCED, false, silUser.DisplayName)); + } + } +} diff --git a/SharpChat/Commands/SilenceRevokeCommand.cs b/SharpChat/Commands/SilenceRevokeCommand.cs new file mode 100644 index 0000000..7d7e1bb --- /dev/null +++ b/SharpChat/Commands/SilenceRevokeCommand.cs @@ -0,0 +1,41 @@ +using SharpChat.Packet; +using System; +using System.Linq; + +namespace SharpChat.Commands { + public class SilenceRevokeCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("unsilence"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(!ctx.User.Can(ChatUserPermissions.SilenceUser)) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; + } + + string unsilUserStr = ctx.Args.FirstOrDefault(); + ChatUser unsilUser; + + lock(ctx.Chat.UsersAccess) + if(string.IsNullOrWhiteSpace(unsilUserStr) || (unsilUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, unsilUserStr ?? "User")); + return; + } + + if(unsilUser.Rank >= ctx.User.Rank) { + ctx.User.Send(new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY)); + return; + } + + if(!unsilUser.IsSilenced) { + ctx.User.Send(new LegacyCommandResponse(LCR.NOT_SILENCED)); + return; + } + + unsilUser.SilencedUntil = DateTimeOffset.MinValue; + unsilUser.Send(new LegacyCommandResponse(LCR.UNSILENCED, false)); + ctx.User.Send(new LegacyCommandResponse(LCR.TARGET_UNSILENCED, false, unsilUser.DisplayName)); + } + } +} diff --git a/SharpChat/Commands/WhisperCommand.cs b/SharpChat/Commands/WhisperCommand.cs new file mode 100644 index 0000000..e55d347 --- /dev/null +++ b/SharpChat/Commands/WhisperCommand.cs @@ -0,0 +1,51 @@ +using SharpChat.Events; +using SharpChat.Packet; +using System; +using System.Linq; + +namespace SharpChat.Commands { + public class WhisperCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("whisper") + || ctx.NameEquals("msg"); + } + + public void Dispatch(ChatCommandContext ctx) { + if(ctx.Args.Length < 2) { + ctx.User.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); + return; + } + + ChatUser whisperUser; + string whisperUserStr = ctx.Args.FirstOrDefault(); + lock(ctx.Chat.UsersAccess) + whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr)); + + if(whisperUser == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, whisperUserStr)); + return; + } + if(whisperUser == ctx.User) + return; + + string whisperStr = string.Join(' ', ctx.Args.Skip(1)); + + whisperUser.Send(new ChatMessageAddPacket(new ChatMessage { + DateTime = DateTimeOffset.Now, + Target = whisperUser, + TargetName = whisperUser.TargetName, + Sender = ctx.User, + Text = whisperStr, + Flags = ChatMessageFlags.Private, + })); + ctx.User.Send(new ChatMessageAddPacket(new ChatMessage { + DateTime = DateTimeOffset.Now, + Target = whisperUser, + TargetName = whisperUser.TargetName, + Sender = ctx.User, + Text = $"{whisperUser.DisplayName} {whisperStr}", + Flags = ChatMessageFlags.Private, + })); + } + } +} diff --git a/SharpChat/Commands/WhoCommand.cs b/SharpChat/Commands/WhoCommand.cs new file mode 100644 index 0000000..4f5e20c --- /dev/null +++ b/SharpChat/Commands/WhoCommand.cs @@ -0,0 +1,65 @@ +using SharpChat.Packet; +using System.Linq; +using System.Text; + +namespace SharpChat.Commands { + public class WhoCommand : IChatCommand { + public bool IsMatch(ChatCommandContext ctx) { + return ctx.NameEquals("who"); + } + + public void Dispatch(ChatCommandContext ctx) { + StringBuilder whoChanSB = new(); + string whoChanStr = ctx.Args.FirstOrDefault(); + + if(string.IsNullOrEmpty(whoChanStr)) { + lock(ctx.Chat.UsersAccess) + foreach(ChatUser whoUser in ctx.Chat.Users) { + whoChanSB.Append(@"'); + whoChanSB.Append(whoUser.DisplayName); + whoChanSB.Append(", "); + } + + if(whoChanSB.Length > 2) + whoChanSB.Length -= 2; + + ctx.User.Send(new LegacyCommandResponse(LCR.USERS_LISTING_SERVER, false, whoChanSB)); + } else { + ChatChannel whoChan; + lock(ctx.Chat.ChannelsAccess) + whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr)); + + if(whoChan == null) { + ctx.User.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); + return; + } + + if(whoChan.Rank > ctx.User.Rank || (whoChan.HasPassword && !ctx.User.Can(ChatUserPermissions.JoinAnyChannel))) { + ctx.User.Send(new LegacyCommandResponse(LCR.USERS_LISTING_ERROR, true, whoChanStr)); + return; + } + + foreach(ChatUser whoUser in whoChan.GetUsers()) { + whoChanSB.Append(@"'); + whoChanSB.Append(whoUser.DisplayName); + whoChanSB.Append(", "); + } + + if(whoChanSB.Length > 2) + whoChanSB.Length -= 2; + + ctx.User.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB)); + } + } + } +} diff --git a/SharpChat/IChatCommand.cs b/SharpChat/IChatCommand.cs index 92f03b0..5a3c883 100644 --- a/SharpChat/IChatCommand.cs +++ b/SharpChat/IChatCommand.cs @@ -1,8 +1,6 @@ -using SharpChat.Events; - -namespace SharpChat { +namespace SharpChat { public interface IChatCommand { - bool IsMatch(string name); - IChatMessage Dispatch(IChatCommandContext context); + bool IsMatch(ChatCommandContext ctx); + void Dispatch(ChatCommandContext ctx); } } diff --git a/SharpChat/IChatCommandContext.cs b/SharpChat/IChatCommandContext.cs deleted file mode 100644 index 5bab770..0000000 --- a/SharpChat/IChatCommandContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace SharpChat { - public interface IChatCommandContext { - IEnumerable Args { get; } - ChatUser User { get; } - ChatChannel Channel { get; } - } - - public class ChatCommandContext : IChatCommandContext { - public IEnumerable Args { get; } - public ChatUser User { get; } - public ChatChannel Channel { get; } - - public ChatCommandContext(IEnumerable args, ChatUser user, ChatChannel channel) { - Args = args ?? throw new ArgumentNullException(nameof(args)); - User = user ?? throw new ArgumentNullException(nameof(user)); - Channel = channel ?? throw new ArgumentNullException(nameof(channel)); - } - } -} diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index a206baf..2181575 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -9,9 +9,7 @@ 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; @@ -41,19 +39,8 @@ namespace SharpChat { private readonly CachedValue MaxConnections; private readonly CachedValue FloodKickLength; - private IReadOnlyCollection Commands { get; } = new IChatCommand[] { - new AFKCommand(), - }; + private List Commands { get; } = new(); - public List Sessions { get; } = new List(); - private object SessionsAccess { get; } = new object(); - - public ChatUserSession GetSession(IWebSocketConnection conn) { - lock(SessionsAccess) - return Sessions.FirstOrDefault(x => x.Connection == conn); - } - - private ManualResetEvent Shutdown { get; set; } private bool IsShuttingDown = false; private ChatChannel DefaultChannel { get; set; } @@ -89,12 +76,35 @@ namespace SharpChat { DefaultChannel ??= channelInfo; } + Commands.AddRange(new IChatCommand[] { + new AFKCommand(), + new NickCommand(), + new WhisperCommand(), + new ActionCommand(), + new WhoCommand(), + new JoinChannelCommand(), + new CreateChannelCommand(), + new DeleteChannelCommand(), + new PasswordChannelCommand(), + new RankChannelCommand(), + new BroadcastCommand(), + new DeleteMessageCommand(), + new KickBanCommand(msz), + new PardonUserCommand(msz), + new PardonAddressCommand(msz), + new BanListCommand(msz), + new SilenceApplyCommand(), + new SilenceRevokeCommand(), + new RemoteAddressCommand(), + }); + ushort port = config.SafeReadValue("port", DEFAULT_PORT); Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}"); } - public void Listen(ManualResetEvent mre) { - Shutdown = mre; + public void Listen(ManualResetEvent waitHandle) { + if(waitHandle != null) + Commands.Add(new ShutdownRestartCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true))); Server.Start(sock => { if(IsShuttingDown || IsDisposed) { @@ -114,9 +124,9 @@ namespace SharpChat { private void OnOpen(IWebSocketConnection conn) { Logger.Write($"Connection opened from {conn.ConnectionInfo.ClientIpAddress}:{conn.ConnectionInfo.ClientPort}"); - lock(SessionsAccess) { - if(!Sessions.Any(x => x.Connection == conn)) - Sessions.Add(new ChatUserSession(conn)); + lock(Context.SessionsAccess) { + if(!Context.Sessions.Any(x => x.Connection == conn)) + Context.Sessions.Add(new ChatUserSession(conn)); } Context.Update(); @@ -125,7 +135,9 @@ namespace SharpChat { private void OnClose(IWebSocketConnection conn) { Logger.Write($"Connection closed from {conn.ConnectionInfo.ClientIpAddress}:{conn.ConnectionInfo.ClientPort}"); - ChatUserSession sess = GetSession(conn); + ChatUserSession sess; + lock(Context.SessionsAccess) + sess = Context.GetSession(conn); // Remove connection from user if(sess?.User != null) { @@ -142,15 +154,19 @@ namespace SharpChat { Context.Update(); // Remove connection from server - lock(SessionsAccess) - Sessions.Remove(sess); + lock(Context.SessionsAccess) + Context.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); + string sessId; + lock(Context.SessionsAccess) { + ChatUserSession sess = Context.GetSession(conn); + sessId = sess?.Id ?? new string('0', ChatUserSession.ID_LENGTH); + } + Logger.Write($"[{sessId} {conn.ConnectionInfo.ClientIpAddress}] {ex}"); Context.Update(); } @@ -162,7 +178,9 @@ namespace SharpChat { private void OnMessage(IWebSocketConnection conn, string msg) { Context.Update(); - ChatUserSession sess = GetSession(conn); + ChatUserSession sess; + lock(Context.SessionsAccess) + sess = Context.GetSession(conn); if(sess == null) { conn.Close(); @@ -339,11 +357,9 @@ namespace SharpChat { 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) + if(!long.TryParse(args[1], out long mUserId) || mUser.UserId != mUserId) break; -#endif ChatChannel mChannel = mUser.CurrentChannel; if(mChannel == null @@ -368,11 +384,25 @@ namespace SharpChat { IChatMessage message = null; - if(messageText[0] == '/') { - message = HandleV1Command(messageText, mUser, mChannel, sess); + if(messageText.StartsWith("/")) { + ChatCommandContext context = new(messageText, Context, mUser, sess, mChannel); - if(message == null) - break; + IChatCommand command = null; + + foreach(IChatCommand cmd in Commands) + if(cmd.IsMatch(context)) { + command = cmd; + break; + } + + if(command != null) { + if(command is ActionCommand actionCommand) + message = actionCommand.ActionDispatch(context); + else { + command.Dispatch(context); + break; + } + } } message ??= new ChatMessage { @@ -391,626 +421,6 @@ namespace SharpChat { } } - 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) { - lock(Context.UsersAccess) - targetUser = Context.Users.FirstOrDefault(u => u.UserId == 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; - - lock(Context.UsersAccess) - if(!string.IsNullOrWhiteSpace(nickStr) && Context.Users.Any(u => u.NameEquals(nickStr))) { - 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; - string whisperUserStr = parts.ElementAtOrDefault(1); - lock(Context.UsersAccess) - whisperUser = Context.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr)); - - if(whisperUser == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, whisperUserStr)); - 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; - lock(Context.ChannelsAccess) - whoChan = Context.Channels.FirstOrDefault(c => c.NameEquals(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 { - lock(Context.UsersAccess) - foreach(ChatUser whoUser in Context.Users) { - 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; - - string joinChanStr = parts.ElementAtOrDefault(1); - ChatChannel joinChan; - lock(Context.ChannelsAccess) - joinChan = Context.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); - - if(joinChan == null) { - user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); - 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)); - - if(!ChatChannel.CheckName(createChanName)) { - user.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID)); - break; - } - - lock(Context.ChannelsAccess) { - if(Context.Channels.Any(c => c.NameEquals(createChanName))) { - user.Send(new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); - break; - } - - ChatChannel createChan = new() { - Name = createChanName, - IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent), - Rank = createChanHierarchy, - Owner = user, - }; - - Context.Channels.Add(createChan); - lock(Context.UsersAccess) { - foreach(ChatUser ccu in Context.Users.Where(u => u.Rank >= channel.Rank)) - ccu.Send(new ChannelCreatePacket(channel)); - } - - 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; - lock(Context.ChannelsAccess) - delChan = Context.Channels.FirstOrDefault(c => c.NameEquals(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; - } - - lock(Context.ChannelsAccess) - Context.RemoveChannel(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; - - lock(Context.ChannelsAccess) - Context.UpdateChannel(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; - } - - lock(Context.ChannelsAccess) - Context.UpdateChannel(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; - } - - lock(Context.EventsAccess) { - IChatEvent delMsg = Context.Events.GetEvent(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.RemoveEvent(delMsg); - Context.Send(new ChatMessageDeletePacket(delMsg.SequenceId)); - } - 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; - - lock(Context.UsersAccess) - if(banUserTarget == null || (banUser = Context.Users.FirstOrDefault(u => u.NameEquals(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; - lock(Context.UsersAccess) - unbanUser = Context.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); - if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { - unbanUserTargetIsName = false; - lock(Context.UsersAccess) - unbanUser = Context.Users.FirstOrDefault(u => u.UserId == 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; - } - - string silUserStr = parts.ElementAtOrDefault(1); - ChatUser silUser; - - lock(Context.UsersAccess) - if(parts.Length < 2 || (silUser = Context.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : silUserStr)); - 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; - } - - string unsilUserStr = parts.ElementAtOrDefault(1); - ChatUser unsilUser; - - lock(Context.UsersAccess) - if(parts.Length < 2 || (unsilUser = Context.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : unsilUserStr)); - 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; - } - - string ipUserStr = parts.ElementAtOrDefault(1); - ChatUser ipUser; - - lock(Context.UsersAccess) - if(parts.Length < 2 || (ipUser = Context.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : ipUserStr)); - 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(SessionsAccess) - 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(); } @@ -1025,8 +435,9 @@ namespace SharpChat { return; IsDisposed = true; - lock(SessionsAccess) - Sessions.ForEach(s => s.Dispose()); + lock(Context.SessionsAccess) + foreach(ChatUserSession sess in Context.Sessions) + sess.Dispose(); Server?.Dispose(); HttpClient?.Dispose();