sharp-chat/SharpChat/SockChatServer.cs

947 lines
40 KiB
C#

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