Turned commands into classes instead of a shitty switch.

This commit is contained in:
flash 2023-02-16 21:34:59 +01:00
parent d1f78a7e8b
commit ea56af0210
26 changed files with 982 additions and 693 deletions

View file

@ -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);
}
}
}

View file

@ -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<ChatChannel> Channels { get; } = new();
public readonly object ChannelsAccess = new();
public HashSet<ChatUserSession> Sessions { get; } = new();
public readonly object SessionsAccess = new();
public HashSet<ChatUser> 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));

View file

@ -83,5 +83,9 @@ namespace SharpChat {
IsDisposed = true;
Connection.Close(CloseCode);
}
public override int GetHashCode() {
return Id.GetHashCode();
}
}
}

View file

@ -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));
}
}
}

View file

@ -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,
};
}
}
}

View file

@ -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();
}
}
}

View file

@ -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)));
}
}
}

View file

@ -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));
}
}
}
}

View file

@ -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));
}
}
}

View file

@ -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));
}
}
}
}

View file

@ -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)));
}
}
}

View file

@ -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();
}
}
}

View file

@ -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));
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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));
}
}
}

View file

@ -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));
}
}
}

View file

@ -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));
}
}
}

View file

@ -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<bool> ShutdownCheck;
public ShutdownRestartCommand(ManualResetEvent waitHandle, Func<bool> 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();
}
}
}

View file

@ -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));
}
}
}

View file

@ -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));
}
}
}

View file

@ -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,
}));
}
}
}

View file

@ -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(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(whoUser == ctx.User)
whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, ");
}
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(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(whoUser == ctx.User)
whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, ");
}
if(whoChanSB.Length > 2)
whoChanSB.Length -= 2;
ctx.User.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
}
}
}
}

View file

@ -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);
}
}

View file

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
namespace SharpChat {
public interface IChatCommandContext {
IEnumerable<string> Args { get; }
ChatUser User { get; }
ChatChannel Channel { get; }
}
public class ChatCommandContext : IChatCommandContext {
public IEnumerable<string> Args { get; }
public ChatUser User { get; }
public ChatChannel Channel { get; }
public ChatCommandContext(IEnumerable<string> 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));
}
}
}

View file

@ -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<int> MaxConnections;
private readonly CachedValue<int> FloodKickLength;
private IReadOnlyCollection<IChatCommand> Commands { get; } = new IChatCommand[] {
new AFKCommand(),
};
private List<IChatCommand> Commands { get; } = new();
public List<ChatUserSession> Sessions { get; } = new List<ChatUserSession>();
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("<", "&lt;")
.Replace(">", "&gt;")
.Replace("\n", " <br/> ");
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(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(whoUser == user)
whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, ");
}
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(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(whoUser == user)
whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, ");
}
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();