Turned commands into classes instead of a shitty switch.
This commit is contained in:
parent
d1f78a7e8b
commit
ea56af0210
26 changed files with 982 additions and 693 deletions
53
SharpChat/ChatCommandContext.cs
Normal file
53
SharpChat/ChatCommandContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -83,5 +83,9 @@ namespace SharpChat {
|
|||
IsDisposed = true;
|
||||
Connection.Close(CloseCode);
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return Id.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
30
SharpChat/Commands/ActionCommand.cs
Normal file
30
SharpChat/Commands/ActionCommand.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
32
SharpChat/Commands/BanListCommand.cs
Normal file
32
SharpChat/Commands/BanListCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
19
SharpChat/Commands/BroadcastCommand.cs
Normal file
19
SharpChat/Commands/BroadcastCommand.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
65
SharpChat/Commands/CreateChannelCommand.cs
Normal file
65
SharpChat/Commands/CreateChannelCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
SharpChat/Commands/DeleteChannelCommand.cs
Normal file
39
SharpChat/Commands/DeleteChannelCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
42
SharpChat/Commands/DeleteMessageCommand.cs
Normal file
42
SharpChat/Commands/DeleteMessageCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
SharpChat/Commands/JoinChannelCommand.cs
Normal file
25
SharpChat/Commands/JoinChannelCommand.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
83
SharpChat/Commands/KickBanCommand.cs
Normal file
83
SharpChat/Commands/KickBanCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
56
SharpChat/Commands/NickCommand.cs
Normal file
56
SharpChat/Commands/NickCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
51
SharpChat/Commands/PardonAddressCommand.cs
Normal file
51
SharpChat/Commands/PardonAddressCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
61
SharpChat/Commands/PardonUserCommand.cs
Normal file
61
SharpChat/Commands/PardonUserCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
26
SharpChat/Commands/PasswordChannelCommand.cs
Normal file
26
SharpChat/Commands/PasswordChannelCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat/Commands/RankChannelCommand.cs
Normal file
28
SharpChat/Commands/RankChannelCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
31
SharpChat/Commands/RemoteAddressCommand.cs
Normal file
31
SharpChat/Commands/RemoteAddressCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
38
SharpChat/Commands/ShutdownRestartCommand.cs
Normal file
38
SharpChat/Commands/ShutdownRestartCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
57
SharpChat/Commands/SilenceApplyCommand.cs
Normal file
57
SharpChat/Commands/SilenceApplyCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
41
SharpChat/Commands/SilenceRevokeCommand.cs
Normal file
41
SharpChat/Commands/SilenceRevokeCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
51
SharpChat/Commands/WhisperCommand.cs
Normal file
51
SharpChat/Commands/WhisperCommand.cs
Normal 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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
65
SharpChat/Commands/WhoCommand.cs
Normal file
65
SharpChat/Commands/WhoCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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("<", "<")
|
||||
.Replace(">", ">")
|
||||
.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();
|
||||
|
|
Loading…
Reference in a new issue