Compare commits

..

40 commits

Author SHA1 Message Date
42a0160cde Changed pretty much every context mutation into an event.
Don't run this in prod yet lol.
2024-05-29 20:51:41 +00:00
6bda1ee09d Only a single clear mode is ever used, removed the rest. 2024-05-28 21:51:54 +00:00
86effa0452 Split status elements out of UserInfo and made user update event based. 2024-05-24 19:17:12 +00:00
2eae48325a Issue user disconnected and kick/ban as events and restructure table. 2024-05-24 16:01:11 +00:00
09d5bfef82 Fixed excessive sending of user update packets.
the funny inverted if condition
2024-05-24 13:19:04 +00:00
d426df91f0 Made the channel event log code similar to the normal event handling code. 2024-05-24 13:11:21 +00:00
5daad52aba Events system overhaul. 2024-05-24 03:44:20 +00:00
454a460441 Removed event flags attribute. 2024-05-24 00:23:31 +00:00
651c3f127d Use interface instead of abstract classes as base for Sock Chat S2C packets. 2024-05-23 23:13:57 +00:00
4ace355374 Removed AddEvent aliases. 2024-05-23 22:31:43 +00:00
968df2b161 Split connection classes. 2024-05-21 20:08:23 +00:00
12e7bd2768 Turns out neither of those two repos were public! 2024-05-21 14:52:15 +00:00
cfbe98d34a Updated README.md. 2024-05-20 23:45:29 +00:00
e0f83ca259 Split various components into sublibraries to avoid things depending on things they should not depend on. 2024-05-20 23:40:34 +00:00
980ec5b855 Apply S2C and C2S naming scheme for easy packet direction identification. 2024-05-20 23:00:47 +00:00
a0e6fbbeea Packet packing micro optimisation. 2024-05-20 16:24:14 +00:00
610f9ab142 Connection handling rewrite. 2024-05-20 16:16:32 +00:00
fa8c416b77 Use server start timestamp for welcome MOTD message and MOTD file last write for the other one. 2024-05-20 02:27:46 +00:00
1d781bd72c Added base class for packets with timestamp. 2024-05-20 02:16:38 +00:00
042b6ddbd6 Removed IServerPacket interface. 2024-05-20 01:35:39 +00:00
c490dcf128 Extracted all log packets into their own ones. 2024-05-20 01:35:33 +00:00
549c80740d Rewrote user and channel collections. 2024-05-19 21:02:17 +00:00
1a8c44a4ba Cleaned up the names of some of the base classes. 2024-05-19 02:17:51 +00:00
bd23d3aa15 Some cleanups (snapshot, don't run this). 2024-05-19 01:53:32 +00:00
68a523f76a Use HasFlag instead of custom Can method. 2024-05-17 23:50:22 +00:00
322500739e Moved some things out of the MessagePopulatePacket class. 2024-05-14 22:56:56 +00:00
a6a7e56bd1 Drew the rest of the fucking owl. 2024-05-14 22:17:25 +00:00
7bcf5acb7e Created more discrete error/response packets. 2024-05-14 19:11:09 +00:00
38f17c325a Apparently markdown doesn't have underlining. 2024-05-14 17:54:59 +00:00
907711e753 Split some packets out of LegacyCommandResponse. 2024-05-13 20:55:54 +00:00
8cc00fe1a8 Updated protocol information document. 2024-05-10 23:50:40 +00:00
3c58e5201a Updated LICENSE year. 2024-05-10 19:29:03 +00:00
795a87fe56 Added migration to update event types of older messages. 2024-05-10 19:23:19 +00:00
a6569815af Enabled explicit nullable. 2024-05-10 19:18:55 +00:00
b95cd06cb1 Reduce usage of working objects in packet objects as much as possible. 2024-05-10 18:29:48 +00:00
356409eb16 Simplified packet building. 2024-05-10 17:28:52 +00:00
1ba94a526c Removed unused method from ChatMessageAddPacket. 2024-05-10 15:25:50 +00:00
0b0de00cc4 Updated MySQL connector library. 2024-05-10 15:24:56 +00:00
b1fae4bdeb Simplified Pack method return type. 2024-05-10 15:24:43 +00:00
fc7d428f76 Split name change notification out of UserUpdatePacket. 2024-05-10 15:07:56 +00:00
200 changed files with 5513 additions and 3712 deletions

4
.editorconfig Normal file
View file

@ -0,0 +1,4 @@
[*.{cs,vb}]
# IDE0046: Convert to conditional expression
dotnet_style_prefer_conditional_expression_over_return = false

View file

@ -53,8 +53,6 @@ User permissions are a set of flags separated by either the form feed character
The reason there are two options is due to a past mixup that we now have to live with.
Which of the methods is used remains consistent per server however, so the result of a test can be cached.
Note that this string MAY be empty if sent by the bot user (`-1`).
| Type | Description |
|:------:| ----------- |
| `int` | Rank of the user. Used to determine what channels a user can access or what other users the user can moderate. |

View file

@ -1,5 +1,4 @@
using System;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace SharpChat.Misuzu {
public class MisuzuAuthInfo {
@ -13,18 +12,18 @@ namespace SharpChat.Misuzu {
public long UserId { get; set; }
[JsonPropertyName("username")]
public string UserName { get; set; }
public string? UserName { get; set; }
[JsonPropertyName("colour_raw")]
public int ColourRaw { get; set; }
public ChatColour Colour => ChatColour.FromMisuzu(ColourRaw);
public Colour Colour => Colour.FromMisuzu(ColourRaw);
[JsonPropertyName("hierarchy")]
public int Rank { get; set; }
[JsonPropertyName("perms")]
public ChatUserPermissions Permissions { get; set; }
public UserPermissions Permissions { get; set; }
[JsonPropertyName("super")]
public bool IsSuper { get; set; }

View file

@ -7,10 +7,10 @@ namespace SharpChat.Misuzu {
public bool IsBanned { get; set; }
[JsonPropertyName("user_id")]
public string UserId { get; set; }
public string? UserId { get; set; }
[JsonPropertyName("ip_addr")]
public string RemoteAddress { get; set; }
public string? RemoteAddress { get; set; }
[JsonPropertyName("is_perma")]
public bool IsPermanent { get; set; }
@ -20,13 +20,13 @@ namespace SharpChat.Misuzu {
// only populated in list request
[JsonPropertyName("user_name")]
public string UserName { get; set; }
public string? UserName { get; set; }
[JsonPropertyName("user_colour")]
public int UserColourRaw { get; set; }
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
public ChatColour UserColour => ChatColour.FromMisuzu(UserColourRaw);
public Colour UserColour => Colour.FromMisuzu(UserColourRaw);
}
}

View file

@ -34,9 +34,7 @@ namespace SharpChat.Misuzu {
private CachedValue<string> SecretKey { get; }
public MisuzuClient(HttpClient httpClient, IConfig config) {
if(config == null)
throw new ArgumentNullException(nameof(config));
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
HttpClient = httpClient;
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
@ -47,11 +45,11 @@ namespace SharpChat.Misuzu {
}
public string CreateBufferSignature(byte[] bytes) {
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey));
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey.Value ?? string.Empty));
return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
}
public async Task<MisuzuAuthInfo> AuthVerifyAsync(string method, string token, string ipAddr) {
public async Task<MisuzuAuthInfo?> AuthVerifyAsync(string method, string token, string ipAddr) {
method ??= string.Empty;
token ??= string.Empty;
ipAddr ??= string.Empty;
@ -77,8 +75,6 @@ namespace SharpChat.Misuzu {
}
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
if(list == null)
throw new ArgumentNullException(nameof(list));
if(!list.Any())
return;
@ -114,7 +110,11 @@ namespace SharpChat.Misuzu {
}
}
public async Task<MisuzuBanInfo> CheckBanAsync(string userId = null, string ipAddr = null, bool userIdIsName = false) {
public async Task<MisuzuBanInfo?> CheckBanAsync(
string? userId = null,
string? ipAddr = null,
bool userIdIsName = false
) {
userId ??= string.Empty;
ipAddr ??= string.Empty;
@ -136,7 +136,7 @@ namespace SharpChat.Misuzu {
);
}
public async Task<MisuzuBanInfo[]> GetBanListAsync() {
public async Task<MisuzuBanInfo[]?> GetBanListAsync() {
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
string sig = string.Format(BANS_LIST_SIG, now);
@ -160,9 +160,6 @@ namespace SharpChat.Misuzu {
}
public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
if(banInfo == null)
throw new ArgumentNullException(nameof(banInfo));
string type = kind switch {
BanRevokeKind.UserId => "user",
BanRevokeKind.RemoteAddress => "addr",
@ -170,8 +167,8 @@ namespace SharpChat.Misuzu {
};
string target = kind switch {
BanRevokeKind.UserId => banInfo.UserId,
BanRevokeKind.RemoteAddress => banInfo.RemoteAddress,
BanRevokeKind.UserId => banInfo?.UserId ?? string.Empty,
BanRevokeKind.RemoteAddress => banInfo?.RemoteAddress ?? string.Empty,
_ => string.Empty,
};
@ -204,9 +201,9 @@ namespace SharpChat.Misuzu {
string reason
) {
if(string.IsNullOrWhiteSpace(targetAddr))
throw new ArgumentNullException(nameof(targetAddr));
throw new ArgumentException("targetAddr may not be empty", nameof(targetAddr));
if(string.IsNullOrWhiteSpace(modAddr))
throw new ArgumentNullException(nameof(modAddr));
throw new ArgumentException("modAddr may not be empty", nameof(modAddr));
if(duration <= TimeSpan.Zero)
return;

View file

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,36 @@
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.SockChat.Commands {
public class BanListCommand : ISockChatClientCommand {
private readonly MisuzuClient Misuzu;
public BanListCommand(MisuzuClient msz) {
Misuzu = msz;
}
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("bans")
|| ctx.NameEquals("banned");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.KickUser)
&& !ctx.User.Permissions.HasFlag(UserPermissions.BanUser)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
Task.Run(async () => {
ctx.Chat.SendTo(ctx.User, new BanListResponseS2CPacket(
(await Misuzu.GetBanListAsync() ?? Array.Empty<MisuzuBanInfo>()).Select(
ban => string.IsNullOrEmpty(ban.UserName) ? (string.IsNullOrEmpty(ban.RemoteAddress) ? string.Empty : ban.RemoteAddress) : ban.UserName
).ToArray()
));
}).Wait();
}
}
}

View file

@ -0,0 +1,66 @@
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class ChannelCreateCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("create");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.CreateChannel)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
string firstArg = ctx.Args.First();
bool createChanHasHierarchy;
if(!ctx.Args.Any() || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return;
}
int createChanHierarchy = 0;
if(createChanHasHierarchy)
if(!int.TryParse(firstArg, out createChanHierarchy))
createChanHierarchy = 0;
if(createChanHierarchy > ctx.User.Rank) {
ctx.Chat.SendTo(ctx.User, new ChannelRankTooHighErrorS2CPacket());
return;
}
string channelName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0));
if(!SockChatUtility.CheckChannelName(channelName)) {
ctx.Chat.SendTo(ctx.User, new ChannelNameFormatErrorS2CPacket());
return;
}
if(ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName) != null) {
ctx.Chat.SendTo(ctx.User, new ChannelNameInUseErrorS2CPacket(channelName));
return;
}
ctx.Chat.Events.Dispatch(
"chan:add",
channelName,
ctx.User,
new ChannelAddEventData(
!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPermanent),
createChanHierarchy,
string.Empty
)
);
DateTimeOffset now = DateTimeOffset.UtcNow;
ctx.Chat.Events.Dispatch("chan:leave", now, ctx.Channel, ctx.User);
ctx.Chat.Events.Dispatch("chan:join", now, channelName, ctx.User);
ctx.Chat.SendTo(ctx.User, new ChannelCreateResponseS2CPacket(channelName));
}
}
}

View file

@ -0,0 +1,36 @@
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class ChannelDeleteCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("delchan") || (
ctx.NameEquals("delete")
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == false
);
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.Args.Any() || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return;
}
string delChanName = string.Join('_', ctx.Args);
ChannelInfo? delChan = ctx.Chat.Channels.Get(delChanName, SockChatUtility.SanitiseChannelName);
if(delChan == null) {
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(delChanName));
return;
}
if(!ctx.User.Permissions.HasFlag(UserPermissions.DeleteChannel) || delChan.OwnerId != ctx.User.UserId) {
ctx.Chat.SendTo(ctx.User, new ChannelDeleteNotAllowedErrorS2CPacket(delChan.Name));
return;
}
ctx.Chat.Events.Dispatch("chan:delete", delChan, ctx.User);
ctx.Chat.SendTo(ctx.User, new ChannelDeleteResponseS2CPacket(delChan.Name));
}
}
}

View file

@ -0,0 +1,48 @@
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class ChannelJoinCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("join");
}
public void Dispatch(SockChatClientCommandContext ctx) {
string channelName = ctx.Args.FirstOrDefault() ?? string.Empty;
string password = string.Join(' ', ctx.Args.Skip(1));
ChannelInfo? channelInfo = ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName);
if(channelInfo == null) {
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(channelName));
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
return;
}
if(channelInfo.Name.Equals(ctx.Channel.Name, StringComparison.InvariantCultureIgnoreCase)) {
// this is where the elusive commented out "samechan" error would go!
// https://patchii.net/sockchat/sockchat/src/commit/6c2111fb4b0241f9ef31060b0f86e7176664f572/server/lib/context.php#L61
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
return;
}
if(!ctx.User.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && channelInfo.OwnerId != ctx.User.UserId) {
if(channelInfo.Rank > ctx.User.Rank) {
ctx.Chat.SendTo(ctx.User, new ChannelRankTooLowErrorS2CPacket(channelInfo.Name));
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
return;
}
if(!string.IsNullOrEmpty(channelInfo.Password) && channelInfo.Password.Equals(password)) {
ctx.Chat.SendTo(ctx.User, new ChannelPasswordWrongErrorS2CPacket(channelInfo.Name));
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
return;
}
}
DateTimeOffset now = DateTimeOffset.UtcNow;
ctx.Chat.Events.Dispatch("chan:leave", now, ctx.Channel, ctx.User);
ctx.Chat.Events.Dispatch("chan:join", now, channelInfo, ctx.User);
}
}
}

View file

@ -0,0 +1,26 @@
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
namespace SharpChat.SockChat.Commands {
public class ChannelPasswordCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("pwd")
|| ctx.NameEquals("password");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPassword) || ctx.Channel.OwnerId != ctx.User.UserId) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
string chanPass = string.Join(' ', ctx.Args).Trim();
if(string.IsNullOrWhiteSpace(chanPass))
chanPass = string.Empty;
ctx.Chat.Events.Dispatch("chan:update", ctx.Channel.Name, ctx.User, new ChannelUpdateEventData(password: chanPass));
ctx.Chat.SendTo(ctx.User, new ChannelPasswordChangedResponseS2CPacket());
}
}
}

View file

@ -0,0 +1,28 @@
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class ChannelRankCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("rank")
|| ctx.NameEquals("privilege")
|| ctx.NameEquals("priv");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelHierarchy) || ctx.Channel.OwnerId != ctx.User.UserId) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
if(!ctx.Args.Any() || !int.TryParse(ctx.Args.First(), out int chanMinRank) || chanMinRank > ctx.User.Rank) {
ctx.Chat.SendTo(ctx.User, new ChannelRankTooHighErrorS2CPacket());
return;
}
ctx.Chat.Events.Dispatch("chan:update", ctx.Channel.Name, ctx.User, new ChannelUpdateEventData(minRank: chanMinRank));
ctx.Chat.SendTo(ctx.User, new ChannelRankChangedResponseS2CPacket());
}
}
}

View file

@ -0,0 +1,6 @@
namespace SharpChat.SockChat.Commands {
public interface ISockChatClientCommand {
bool IsMatch(SockChatClientCommandContext ctx);
void Dispatch(SockChatClientCommandContext ctx);
}
}

View file

@ -0,0 +1,86 @@
using SharpChat.Events;
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.SockChat.Commands {
public class KickBanCommand : ISockChatClientCommand {
private readonly MisuzuClient Misuzu;
public KickBanCommand(MisuzuClient msz) {
Misuzu = msz;
}
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("kick")
|| ctx.NameEquals("ban");
}
public void Dispatch(SockChatClientCommandContext ctx) {
bool isBanning = ctx.NameEquals("ban");
if(!ctx.User.Permissions.HasFlag(isBanning ? UserPermissions.BanUser : UserPermissions.KickUser)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
string banUserTarget = ctx.Args.ElementAtOrDefault(0) ?? string.Empty;
string? banDurationStr = ctx.Args.ElementAtOrDefault(1);
int banReasonIndex = 1;
UserInfo? banUser = null;
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(banUserTarget);
if(string.IsNullOrEmpty(name) || (banUser = ctx.Chat.Users.Get(name: name, nameTarget: target)) == null) {
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorS2CPacket(banUserTarget));
return;
}
if(!ctx.User.IsSuper && banUser.Rank >= ctx.User.Rank && banUser != ctx.User) {
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorS2CPacket(SockChatUtility.GetUserName(banUser)));
return;
}
TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero;
if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) {
if(durationSeconds < 0) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return;
}
duration = TimeSpan.FromSeconds(durationSeconds);
++banReasonIndex;
}
if(duration <= TimeSpan.Zero) {
ctx.Chat.Events.Dispatch("user:kickban", banUser, UserKickBanEventData.OfDuration(UserDisconnectReason.Kicked, TimeSpan.Zero));
return;
}
string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex));
Task.Run(async () => {
string userId = banUser.UserId.ToString();
MisuzuBanInfo? fbi = await Misuzu.CheckBanAsync(userId);
if(fbi != null && fbi.IsBanned && !fbi.HasExpired) {
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorS2CPacket(SockChatUtility.GetUserName(banUser)));
return;
}
string[] userRemoteAddrs = ctx.Chat.Connections.GetUserRemoteAddresses(banUser);
string userRemoteAddr = string.Format(", ", userRemoteAddrs);
// Misuzu only stores the IP address in private comment and doesn't do any checking, so this is fine.
await Misuzu.CreateBanAsync(
userId, userRemoteAddr,
ctx.User.UserId.ToString(), ctx.Connection.RemoteAddress,
duration, banReason
);
ctx.Chat.Events.Dispatch("user:kickban", banUser, UserKickBanEventData.OfDuration(UserDisconnectReason.Kicked, duration));
}).Wait();
}
}
}

View file

@ -0,0 +1,22 @@
using SharpChat.Events;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class MessageActionCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("action")
|| ctx.NameEquals("me");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.Args.Any())
return;
string actionStr = string.Join(' ', ctx.Args);
if(string.IsNullOrWhiteSpace(actionStr))
return;
ctx.Chat.Events.Dispatch("msg:add", ctx.Channel, ctx.User, new MessageAddEventData(actionStr, true));
}
}
}

View file

@ -0,0 +1,24 @@
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
namespace SharpChat.SockChat.Commands {
public class MessageBroadcastCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("say")
|| ctx.NameEquals("broadcast");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.Broadcast)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
ctx.Chat.Events.Dispatch(
"msg:add",
ctx.User,
new MessageAddEventData(string.Join(' ', ctx.Args))
);
}
}
}

View file

@ -0,0 +1,42 @@
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class MessageDeleteCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("delmsg") || (
ctx.NameEquals("delete")
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == true
);
}
public void Dispatch(SockChatClientCommandContext ctx) {
bool deleteAnyMessage = ctx.User.Permissions.HasFlag(UserPermissions.DeleteAnyMessage);
if(!deleteAnyMessage && !ctx.User.Permissions.HasFlag(UserPermissions.DeleteOwnMessage)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
string? firstArg = ctx.Args.FirstOrDefault();
if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long eventId)) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return;
}
ChatEventInfo? eventInfo = ctx.Chat.EventStorage.GetEvent(eventId);
if(eventInfo == null
|| !eventInfo.Type.Equals("msg:add")
|| eventInfo.SenderRank > ctx.User.Rank
|| (!deleteAnyMessage && eventInfo.SenderId != ctx.User.UserId)) {
ctx.Chat.SendTo(ctx.User, new MessageDeleteNotAllowedErrorS2CPacket());
return;
}
ctx.Chat.Events.Dispatch("msg:delete", eventInfo.ChannelName, ctx.User, new MessageDeleteEventData(eventInfo.Id.ToString()));
}
}
}

View file

@ -0,0 +1,38 @@
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class MessageWhisperCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("whisper")
|| ctx.NameEquals("msg");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(ctx.Args.Length < 2) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return;
}
string whisperUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(whisperUserStr);
UserInfo? whisperUser = ctx.Chat.Users.Get(name: name, nameTarget: target);
if(whisperUser == null) {
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorS2CPacket(whisperUserStr));
return;
}
if(whisperUser == ctx.User)
return;
ctx.Chat.Events.Dispatch(
"msg:add",
UserInfo.GetDMChannelName(ctx.User, whisperUser),
ctx.User,
new MessageAddEventData(string.Join(' ', ctx.Args.Skip(1)))
);
}
}
}

View file

@ -0,0 +1,51 @@
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace SharpChat.SockChat.Commands {
public class PardonAddressCommand : ISockChatClientCommand {
private readonly MisuzuClient Misuzu;
public PardonAddressCommand(MisuzuClient msz) {
Misuzu = msz;
}
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("pardonip")
|| ctx.NameEquals("unbanip");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.KickUser)
&& !ctx.User.Permissions.HasFlag(UserPermissions.BanUser)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
string? unbanAddrTarget = ctx.Args.FirstOrDefault();
if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress? unbanAddr)) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return;
}
unbanAddrTarget = unbanAddr.ToString();
Task.Run(async () => {
MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget);
if(banInfo?.IsBanned != true || banInfo.HasExpired) {
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanAddrTarget));
return;
}
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress);
if(wasBanned)
ctx.Chat.SendTo(ctx.User, new PardonResponseS2CPacket(unbanAddrTarget));
else
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanAddrTarget));
}).Wait();
}
}
}

View file

@ -0,0 +1,59 @@
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.SockChat.Commands {
public class PardonUserCommand : ISockChatClientCommand {
private readonly MisuzuClient Misuzu;
public PardonUserCommand(MisuzuClient msz) {
Misuzu = msz;
}
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("pardon")
|| ctx.NameEquals("unban");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.KickUser)
&& !ctx.User.Permissions.HasFlag(UserPermissions.BanUser)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
bool unbanUserTargetIsName = true;
string? unbanUserTarget = ctx.Args.FirstOrDefault();
if(string.IsNullOrWhiteSpace(unbanUserTarget)) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return;
}
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(unbanUserTarget);
UserInfo? unbanUser = ctx.Chat.Users.Get(name: name, nameTarget: target);
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
unbanUserTargetIsName = false;
unbanUser = ctx.Chat.Users.Get(unbanUserId);
}
if(unbanUser != null)
unbanUserTarget = unbanUser.UserId.ToString();
Task.Run(async () => {
MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName);
if(banInfo?.IsBanned != true || banInfo.HasExpired) {
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanUserTarget));
return;
}
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId);
if(wasBanned)
ctx.Chat.SendTo(ctx.User, new PardonResponseS2CPacket(unbanUserTarget));
else
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanUserTarget));
}).Wait();
}
}
}

View file

@ -0,0 +1,40 @@
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Threading;
namespace SharpChat.SockChat.Commands {
public class ShutdownRestartCommand : ISockChatClientCommand {
private readonly ManualResetEvent WaitHandle;
private readonly Func<bool> ShuttingDown;
private readonly Action<bool> SetShutdown;
public ShutdownRestartCommand(
ManualResetEvent waitHandle,
Func<bool> shuttingDown,
Action<bool> setShutdown
) {
WaitHandle = waitHandle;
ShuttingDown = shuttingDown;
SetShutdown = setShutdown;
}
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("shutdown")
|| ctx.NameEquals("restart");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(ctx.User.UserId != 1) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
if(ShuttingDown())
return;
SetShutdown(ctx.NameEquals("restart"));
ctx.Chat.Update();
WaitHandle?.Set();
}
}
}

View file

@ -0,0 +1,34 @@
using System;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class SockChatClientCommandContext {
public string Name { get; }
public string[] Args { get; }
public SockChatContext Chat { get; }
public UserInfo User { get; }
public ConnectionInfo Connection { get; }
public ChannelInfo Channel { get; }
public SockChatClientCommandContext(
string text,
SockChatContext chat,
UserInfo user,
ConnectionInfo connection,
ChannelInfo channel
) {
Chat = chat;
User = user;
Connection = connection;
Channel = channel;
string[] parts = text[1..].Split(' ');
Name = parts.First().Replace(".", string.Empty);
Args = parts.Skip(1).ToArray();
}
public bool NameEquals(string name) {
return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View file

@ -1,17 +1,17 @@
using SharpChat.Packet;
using SharpChat.Events;
using System.Linq;
namespace SharpChat.Commands {
public class AFKCommand : IChatCommand {
namespace SharpChat.SockChat.Commands {
public class UserAFKCommand : ISockChatClientCommand {
private const string DEFAULT = "AFK";
private const int MAX_LENGTH = 5;
public bool IsMatch(ChatCommandContext ctx) {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("afk");
}
public void Dispatch(ChatCommandContext ctx) {
string statusText = ctx.Args.FirstOrDefault();
public void Dispatch(SockChatClientCommandContext ctx) {
string? statusText = ctx.Args.FirstOrDefault();
if(string.IsNullOrWhiteSpace(statusText))
statusText = DEFAULT;
else {
@ -20,10 +20,10 @@ namespace SharpChat.Commands {
statusText = statusText[..MAX_LENGTH].Trim();
}
ctx.Chat.UpdateUser(
ctx.Chat.Events.Dispatch(
"user:status",
ctx.User,
status: ChatUserStatus.Away,
statusText: statusText
new UserStatusUpdateEventData(UserStatus.Away, statusText)
);
}
}

View file

@ -0,0 +1,61 @@
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class UserNickCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("nick");
}
public void Dispatch(SockChatClientCommandContext ctx) {
bool setOthersNick = ctx.User.Permissions.HasFlag(UserPermissions.SetOthersNickname);
if(!setOthersNick && !ctx.User.Permissions.HasFlag(UserPermissions.SetOwnNickname)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
UserInfo? targetUser = null;
int offset = 0;
if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) {
targetUser = ctx.Chat.Users.Get(targetUserId);
++offset;
}
targetUser ??= ctx.User;
if(ctx.Args.Length < offset) {
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
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 = string.Empty;
else if(nickStr.Length > 15)
nickStr = nickStr[..15];
if(string.IsNullOrWhiteSpace(nickStr))
nickStr = string.Empty;
else if(ctx.Chat.Users.Get(name: nickStr, nameTarget: UsersContext.NameTarget.UserAndNickName) != null) {
ctx.Chat.SendTo(ctx.User, new UserNameInUseErrorS2CPacket(nickStr));
return;
}
ctx.Chat.Events.Dispatch(
"user:update",
targetUser,
new UserUpdateEventData(
nickName: nickStr,
notify: targetUser.UserId == ctx.User.UserId
)
);
}
}
}

View file

@ -0,0 +1,44 @@
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class WhoCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("who");
}
public void Dispatch(SockChatClientCommandContext ctx) {
string? channelName = ctx.Args.FirstOrDefault();
if(string.IsNullOrEmpty(channelName)) {
ctx.Chat.SendTo(ctx.User, new WhoServerResponseS2CPacket(
ctx.Chat.Users.All.Select(u => SockChatUtility.GetUserName(u, ctx.Chat.UserStatuses.Get(u))).ToArray(),
SockChatUtility.GetUserName(ctx.User)
));
return;
}
ChannelInfo? channel = ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName);
if(channel == null) {
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(channelName));
return;
}
if(channel.Rank > ctx.User.Rank || (channel.HasPassword && !ctx.User.Permissions.HasFlag(UserPermissions.JoinAnyChannel))) {
ctx.Chat.SendTo(ctx.User, new WhoChannelNotFoundErrorS2CPacket(channelName));
return;
}
UserInfo[] userInfos = ctx.Chat.Users.GetMany(
ctx.Chat.ChannelsUsers.GetChannelUserIds(channel)
);
ctx.Chat.SendTo(ctx.User, new WhoChannelResponseS2CPacket(
channel.Name,
userInfos.Select(user => SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user))).ToArray(),
SockChatUtility.GetUserName(ctx.User, ctx.Chat.UserStatuses.Get(ctx.User))
));
}
}
}

View file

@ -0,0 +1,30 @@
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class WhoisCommand : ISockChatClientCommand {
public bool IsMatch(SockChatClientCommandContext ctx) {
return ctx.NameEquals("ip")
|| ctx.NameEquals("whois");
}
public void Dispatch(SockChatClientCommandContext ctx) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.SeeIPAddress)) {
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
return;
}
string ipUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
UserInfo? ipUser;
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(ipUserStr);
if(string.IsNullOrWhiteSpace(name) || (ipUser = ctx.Chat.Users.Get(name: name, nameTarget: target)) == null) {
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorS2CPacket(ipUserStr));
return;
}
foreach(string remoteAddr in ctx.Chat.Connections.GetUserRemoteAddresses(ipUser))
ctx.Chat.SendTo(ctx.User, new WhoisResponseS2CPacket(ipUser.UserName, remoteAddr));
}
}
}

View file

@ -0,0 +1,237 @@
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.SockChat.PacketsC2S {
public class AuthC2SPacketHandler : IC2SPacketHandler {
public const string MOTD_FILE = @"welcome.txt";
private readonly DateTimeOffset Started;
private readonly MisuzuClient Misuzu;
private readonly ChannelInfo DefaultChannel;
private readonly CachedValue<string> MOTDHeaderFormat;
private readonly CachedValue<int> MaxMessageLength;
private readonly CachedValue<int> MaxConnections;
public AuthC2SPacketHandler(
DateTimeOffset started,
MisuzuClient msz,
ChannelInfo? defaultChannel,
CachedValue<string> motdHeaderFormat,
CachedValue<int> maxMsgLength,
CachedValue<int> maxConns
) {
Started = started;
Misuzu = msz;
DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
MOTDHeaderFormat = motdHeaderFormat;
MaxMessageLength = maxMsgLength;
MaxConnections = maxConns;
}
public bool IsMatch(C2SPacketHandlerContext ctx) {
return ctx.CheckPacketId("1");
}
public void Handle(C2SPacketHandlerContext ctx) {
string[] args = ctx.SplitText(3);
string? authMethod = args.ElementAtOrDefault(1);
if(string.IsNullOrWhiteSpace(authMethod)) {
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
ctx.Connection.Close(1000);
return;
}
string? authToken = args.ElementAtOrDefault(2);
if(string.IsNullOrWhiteSpace(authToken)) {
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
ctx.Connection.Close(1000);
return;
}
if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) {
string[] tokenParts = authToken.Split(':', 2);
authMethod = tokenParts[0];
authToken = tokenParts[1];
}
Task.Run(async () => {
MisuzuAuthInfo? fai;
string ipAddr = ctx.Connection.RemoteAddress;
try {
fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr);
} catch(Exception ex) {
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> Failed to authenticate: {ex}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
ctx.Connection.Close(1000);
#if DEBUG
throw;
#else
return;
#endif
}
if(fai == null) {
Logger.Debug($"<{ctx.Connection.RemoteEndPoint}> Auth fail: <null>");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.Null));
ctx.Connection.Close(1000);
return;
}
if(!fai.Success) {
Logger.Debug($"<{ctx.Connection.RemoteEndPoint}> Auth fail: {fai.Reason}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
ctx.Connection.Close(1000);
return;
}
MisuzuBanInfo? fbi;
try {
fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr);
} catch(Exception ex) {
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> Failed auth ban check: {ex}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
ctx.Connection.Close(1000);
#if DEBUG
throw;
#else
return;
#endif
}
if(fbi == null) {
Logger.Debug($"<{ctx.Connection.RemoteEndPoint}> Ban check fail: <null>");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.Null));
ctx.Connection.Close(1000);
return;
}
if(fbi.IsBanned && !fbi.HasExpired) {
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> User is banned.");
ctx.Connection.Send(new AuthFailS2CPacket(fbi.ExpiresAt));
ctx.Connection.Close(1000);
return;
}
if(ctx.Chat.Connections.GetCountForUser(fai.UserId) >= MaxConnections) {
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.MaxSessions));
ctx.Connection.Close(1000);
return;
}
await ctx.Chat.ContextAccess.WaitAsync();
try {
UserInfo? user = ctx.Chat.Users.Get(fai.UserId);
if(user == null) {
ctx.Chat.Events.Dispatch(
"user:add",
fai.UserId,
fai.UserName ?? string.Empty,
fai.Colour,
fai.Rank,
string.Empty,
fai.Permissions,
new UserAddEventData(fai.IsSuper)
);
user = ctx.Chat.Users.Get(fai.UserId);
if(user == null) {
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> User didn't get added.");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.Null));
ctx.Connection.Close(1000);
return;
}
} else {
string? updName = !user.UserName.Equals(fai.UserName) ? fai.UserName : null;
int? updColour = (updColour = fai.Colour.ToMisuzu()) != user.Colour.ToMisuzu() ? updColour : null;
int? updRank = user.Rank != fai.Rank ? fai.Rank : null;
UserPermissions? updPerms = user.Permissions != fai.Permissions ? fai.Permissions : null;
bool? updSuper = user.IsSuper != fai.IsSuper ? fai.IsSuper : null;
if(updName != null || updColour != null || updRank != null || updPerms != null || updSuper != null)
ctx.Chat.Events.Dispatch(
"user:update",
user,
new UserUpdateEventData(
name: updName,
colour: updColour,
rank: updRank,
perms: updPerms,
isSuper: updSuper
)
);
}
ctx.Connection.BumpPing();
ctx.Chat.Connections.SetUser(ctx.Connection, user);
if(!string.IsNullOrWhiteSpace(MOTDHeaderFormat.Value))
ctx.Connection.Send(new MOTDS2CPacket(Started, string.Format(MOTDHeaderFormat.Value, user.UserName)));
if(File.Exists(MOTD_FILE)) {
IEnumerable<string> lines = File.ReadAllLines(MOTD_FILE).Where(x => !string.IsNullOrWhiteSpace(x));
string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count()));
if(!string.IsNullOrWhiteSpace(line))
ctx.Connection.Send(new MOTDS2CPacket(File.GetLastWriteTimeUtc(MOTD_FILE), line));
}
ctx.Connection.Send(new AuthSuccessS2CPacket(
user.UserId,
SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user)),
user.Colour,
user.Rank,
user.Permissions,
DefaultChannel.Name,
MaxMessageLength
));
UserInfo[] chanUsers = ctx.Chat.Users.GetMany(
ctx.Chat.ChannelsUsers.GetChannelUserIds(DefaultChannel)
);
List<UsersPopulateS2CPacket.ListEntry> chanUserEntries = new();
foreach(UserInfo chanUserInfo in chanUsers)
if(chanUserInfo.UserId != user.UserId)
chanUserEntries.Add(new(
chanUserInfo.UserId,
SockChatUtility.GetUserName(chanUserInfo, ctx.Chat.UserStatuses.Get(chanUserInfo)),
chanUserInfo.Colour,
chanUserInfo.Rank,
chanUserInfo.Permissions,
true
));
ctx.Connection.Send(new UsersPopulateS2CPacket(chanUserEntries.ToArray()));
ctx.Chat.Events.Dispatch(
"user:connect",
DefaultChannel,
user,
new UserConnectEventData(!ctx.Chat.ChannelsUsers.Has(DefaultChannel, user))
);
ctx.Chat.HandleChannelEventLog(DefaultChannel.Name, p => ctx.Connection.Send(p));
ChannelInfo[] chans = ctx.Chat.Channels.GetMany(isPublic: true, minRank: user.Rank);
List<ChannelsPopulateS2CPacket.ListEntry> chanEntries = new();
foreach(ChannelInfo chanInfo in chans)
chanEntries.Add(new(
chanInfo.Name,
chanInfo.HasPassword,
chanInfo.IsTemporary
));
ctx.Connection.Send(new ChannelsPopulateS2CPacket(chanEntries.ToArray()));
} finally {
ctx.Chat.ContextAccess.Release();
}
}).Wait();
}
}
}

View file

@ -0,0 +1,21 @@
namespace SharpChat.SockChat.PacketsC2S {
public class C2SPacketHandlerContext {
public string Text { get; }
public SockChatContext Chat { get; }
public SockChatConnectionInfo Connection { get; }
public C2SPacketHandlerContext(string text, SockChatContext chat, SockChatConnectionInfo connection) {
Text = text;
Chat = chat;
Connection = connection;
}
public bool CheckPacketId(string packetId) {
return Text == packetId || Text.StartsWith(packetId + '\t');
}
public string[] SplitText(int expect) {
return Text.Split('\t', expect + 1);
}
}
}

View file

@ -0,0 +1,6 @@
namespace SharpChat.SockChat.PacketsC2S {
public interface IC2SPacketHandler {
bool IsMatch(C2SPacketHandlerContext ctx);
void Handle(C2SPacketHandlerContext ctx);
}
}

View file

@ -0,0 +1,60 @@
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.SockChat.PacketsC2S {
public class PingC2SPacketHandler : IC2SPacketHandler {
private readonly MisuzuClient Misuzu;
private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1);
private DateTimeOffset LastBump = DateTimeOffset.MinValue;
public PingC2SPacketHandler(MisuzuClient msz) {
Misuzu = msz;
}
public bool IsMatch(C2SPacketHandlerContext ctx) {
return ctx.CheckPacketId("0");
}
public void Handle(C2SPacketHandlerContext ctx) {
string[] parts = ctx.SplitText(2);
if(!int.TryParse(parts.FirstOrDefault(), out int pTime))
return;
ctx.Connection.BumpPing();
ctx.Connection.Send(new PongS2CPacket());
ctx.Chat.ContextAccess.Wait();
try {
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
List<(string, string)> bumpList = new();
foreach(UserInfo userInfo in ctx.Chat.Users.All) {
if(ctx.Chat.UserStatuses.GetStatus(userInfo) != UserStatus.Online)
continue;
string[] remoteAddrs = ctx.Chat.Connections.GetUserRemoteAddresses(userInfo);
if(remoteAddrs.Length < 1)
continue;
bumpList.Add((userInfo.UserId.ToString(), remoteAddrs[0]));
}
if(bumpList.Count > 0)
Task.Run(async () => {
await Misuzu.BumpUsersOnlineAsync(bumpList);
}).Wait();
LastBump = DateTimeOffset.UtcNow;
}
} finally {
ctx.Chat.ContextAccess.Release();
}
}
}
}

View file

@ -0,0 +1,92 @@
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.SockChat.Commands;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.SockChat.PacketsC2S {
public class SendMessageC2SPacketHandler : IC2SPacketHandler {
private readonly CachedValue<int> MaxMessageLength;
private List<ISockChatClientCommand> Commands { get; } = new();
public SendMessageC2SPacketHandler(CachedValue<int> maxMsgLength) {
MaxMessageLength = maxMsgLength;
}
public void AddCommand(ISockChatClientCommand command) {
Commands.Add(command);
}
public void AddCommands(IEnumerable<ISockChatClientCommand> commands) {
Commands.AddRange(commands);
}
public bool IsMatch(C2SPacketHandlerContext ctx) {
return ctx.CheckPacketId("2");
}
public void Handle(C2SPacketHandlerContext ctx) {
string[] args = ctx.SplitText(3);
UserInfo? user = ctx.Chat.Users.Get(ctx.Connection.UserId);
// No longer concats everything after index 1 with \t, no previous implementation did that either
string? messageText = args.ElementAtOrDefault(2);
if(user == null || !user.Permissions.HasFlag(UserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText))
return;
// Extra validation step, not necessary at all but enforces proper formatting in SCv1.
if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId)
return;
ctx.Chat.ContextAccess.Wait();
try {
ChannelInfo? channelInfo = ctx.Chat.Channels.Get(
ctx.Chat.ChannelsUsers.GetUserLastChannel(user)
);
if(channelInfo == null)
return;
if(ctx.Chat.UserStatuses.GetStatus(user) != UserStatus.Online)
ctx.Chat.Events.Dispatch(
"user:status",
user,
new UserStatusUpdateEventData(UserStatus.Online)
);
int maxMsgLength = MaxMessageLength;
if(messageText.Length > maxMsgLength)
messageText = messageText[..maxMsgLength];
messageText = messageText.Trim();
#if DEBUG
Logger.Write($"<{user.UserId} {user.UserName}> {messageText}");
#endif
if(messageText.StartsWith("/")) {
SockChatClientCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channelInfo);
ISockChatClientCommand? command = null;
foreach(ISockChatClientCommand cmd in Commands)
if(cmd.IsMatch(context)) {
command = cmd;
break;
}
if(command != null) {
command.Dispatch(context);
return;
}
}
ctx.Chat.Events.Dispatch("msg:add", channelInfo, user, new MessageAddEventData(messageText));
} finally {
ctx.Chat.ContextAccess.Release();
}
}
}
}

View file

@ -0,0 +1,38 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class AuthFailS2CPacket : ISockChatS2CPacket {
public enum FailReason {
AuthInvalid,
MaxSessions,
Banned,
Null,
}
private readonly FailReason Reason;
private readonly long Expires;
public AuthFailS2CPacket(FailReason reason) {
Reason = reason;
}
public AuthFailS2CPacket(DateTimeOffset expires) {
Reason = FailReason.Banned;
Expires = expires.Year >= 2100 ? -1 : expires.ToUnixTimeSeconds();
}
public string Pack() {
string packet = string.Format("1\tn\t{0}fail", Reason switch {
FailReason.AuthInvalid => "auth",
FailReason.MaxSessions => "sock",
FailReason.Banned => "join",
_ => "user",
});
if(Reason == FailReason.Banned)
packet += string.Format("\t{0}", Expires);
return packet;
}
}
}

View file

@ -0,0 +1,47 @@
namespace SharpChat.SockChat.PacketsS2C {
public class AuthSuccessS2CPacket : ISockChatS2CPacket {
private readonly long UserId;
private readonly string UserName;
private readonly Colour UserColour;
private readonly int UserRank;
private readonly UserPermissions UserPerms;
private readonly string ChannelName;
private readonly int MaxMessageLength;
public AuthSuccessS2CPacket(
long userId,
string userName,
Colour userColour,
int userRank,
UserPermissions userPerms,
string channelName,
int maxMsgLength
) {
UserId = userId;
UserName = userName;
UserColour = userColour;
UserRank = userRank;
UserPerms = userPerms;
ChannelName = channelName;
MaxMessageLength = maxMsgLength;
}
public string Pack() {
return string.Format(
"1\ty\t{0}\t{1}\t{2}\t{3} {4} {5} {6} {7}\t{8}\t{9}",
UserId,
UserName,
UserColour,
UserRank,
UserPerms.HasFlag(UserPermissions.KickUser) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.ViewLogs) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.SetOwnNickname) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.CreateChannel) ? (
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
) : 0,
SockChatUtility.SanitiseChannelName(ChannelName),
MaxMessageLength
);
}
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Text;
namespace SharpChat.SockChat.PacketsS2C {
public class BanListResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string[] Bans;
public BanListResponseS2CPacket(string[] bans) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
Bans = bans;
}
public string Pack() {
StringBuilder sb = new();
sb.AppendFormat("2\t{0}\t-1\t0\fbanlist\f", TimeStamp.ToUnixTimeSeconds());
foreach(string ban in Bans)
sb.AppendFormat(@"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('/unban '+ this.innerHTML);"">{0}</a>, ", ban);
if(Bans.Length > 0)
sb.Length -= 2;
sb.AppendFormat("\t{0}\t10010", MessageId);
return sb.ToString();
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelCreateResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public ChannelCreateResponseS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fcrchan\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,26 @@
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelCreateS2CPacket : ISockChatS2CPacket {
private readonly string ChannelName;
private readonly bool ChannelHasPassword;
private readonly bool ChannelIsTemporary;
public ChannelCreateS2CPacket(
string channelName,
bool channelHasPassword,
bool channelIsTemporary
) {
ChannelName = channelName;
ChannelHasPassword = channelHasPassword;
ChannelIsTemporary = channelIsTemporary;
}
public string Pack() {
return string.Format(
"4\t0\t{0}\t{1}\t{2}",
SockChatUtility.SanitiseChannelName(ChannelName),
ChannelHasPassword ? 1 : 0,
ChannelIsTemporary ? 1 : 0
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelDeleteNotAllowedErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public ChannelDeleteNotAllowedErrorS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fndchan\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelDeleteResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public ChannelDeleteResponseS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fdelchan\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,16 @@
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelDeleteS2CPacket : ISockChatS2CPacket {
private readonly string ChannelName;
public ChannelDeleteS2CPacket(string channelName) {
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"4\t2\t{0}",
SockChatUtility.SanitiseChannelName(ChannelName)
);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelNameFormatErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
public ChannelNameFormatErrorS2CPacket() {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\finchan\t{1}\t10010",
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelNameInUseErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public ChannelNameInUseErrorS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fnischan\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelNotFoundErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public ChannelNotFoundErrorS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fnochan\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelPasswordChangedResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
public ChannelPasswordChangedResponseS2CPacket() {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fcpwdchan\t{1}\t10010",
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelPasswordWrongErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public ChannelPasswordWrongErrorS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fipwchan\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelRankChangedResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
public ChannelRankChangedResponseS2CPacket() {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fcprivchan\t{1}\t10010",
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelRankTooHighErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
public ChannelRankTooHighErrorS2CPacket() {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\frankerr\t{1}\t10010",
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelRankTooLowErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public ChannelRankTooLowErrorS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fipchan\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,30 @@
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelUpdateS2CPacket : ISockChatS2CPacket {
private readonly string ChannelNamePrevious;
private readonly string ChannelNameNew;
private readonly bool ChannelHasPassword;
private readonly bool ChannelIsTemporary;
public ChannelUpdateS2CPacket(
string channelNamePrevious,
string channelNameNew,
bool channelHasPassword,
bool channelIsTemporary
) {
ChannelNamePrevious = channelNamePrevious;
ChannelNameNew = channelNameNew;
ChannelHasPassword = channelHasPassword;
ChannelIsTemporary = channelIsTemporary;
}
public string Pack() {
return string.Format(
"4\t1\t{0}\t{1}\t{2}\t{3}",
SockChatUtility.SanitiseChannelName(ChannelNamePrevious),
SockChatUtility.SanitiseChannelName(ChannelNameNew),
ChannelHasPassword ? 1 : 0,
ChannelIsTemporary ? 1 : 0
);
}
}
}

View file

@ -0,0 +1,29 @@
using System.Text;
namespace SharpChat.SockChat.PacketsS2C {
public class ChannelsPopulateS2CPacket : ISockChatS2CPacket {
public record ListEntry(string Name, bool HasPassword, bool IsTemporary);
private readonly ListEntry[] Entries;
public ChannelsPopulateS2CPacket(ListEntry[] entries) {
Entries = entries;
}
public string Pack() {
StringBuilder sb = new();
sb.AppendFormat("7\t2\t{0}", Entries.Length);
foreach(ListEntry entry in Entries)
sb.AppendFormat(
"\t{0}\t{1}\t{2}",
SockChatUtility.SanitiseChannelName(entry.Name),
entry.HasPassword ? 1 : 0,
entry.IsTemporary ? 1 : 0
);
return sb.ToString();
}
}
}

View file

@ -0,0 +1,7 @@
namespace SharpChat.SockChat.PacketsS2C {
public class ClearMessagesAndUsersS2CPacket : ISockChatS2CPacket {
public string Pack() {
return "8\t3";
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class CommandFormatErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
public CommandFormatErrorS2CPacket() {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fcmdna\t{1}\t10010",
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class CommandNotAllowedErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string CommandName;
public CommandNotAllowedErrorS2CPacket(string commandName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
CommandName = commandName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fcmdna\f/{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
CommandName,
MessageId
);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class FloodWarningS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
public FloodWarningS2CPacket() {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fflwarn\t{1}\t10010",
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,19 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class ForceDisconnectS2CPacket : ISockChatS2CPacket {
private readonly long Expires;
public ForceDisconnectS2CPacket(DateTimeOffset expires) {
Expires = expires <= DateTimeOffset.UtcNow
? 0 : (expires.Year >= 2100 ? -1 : expires.ToUnixTimeSeconds());
}
public string Pack() {
if(Expires != 0)
return string.Format("9\t1\t{0}", Expires);
return "9\t0";
}
}
}

View file

@ -0,0 +1,5 @@
namespace SharpChat.SockChat.PacketsS2C {
public interface ISockChatS2CPacket {
string Pack();
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class KickBanNoRecordErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string TargetName;
public KickBanNoRecordErrorS2CPacket(string targetName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
TargetName = targetName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fnotban\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
TargetName,
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class KickBanNotAllowedErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
public KickBanNotAllowedErrorS2CPacket(string userName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
UserName = userName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fkickna\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
UserName,
MessageId
);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class MOTDS2CPacket : ISockChatS2CPacket {
private readonly DateTimeOffset TimeStamp;
private readonly string Body;
public MOTDS2CPacket(DateTimeOffset timeStamp, string body) {
TimeStamp = timeStamp;
Body = body;
}
public string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fsay\f{1}\twelcome\t0\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseMessageBody(Body)
);
}
}
}

View file

@ -0,0 +1,83 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class MessageAddLogS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly long UserId;
private readonly string UserName;
private readonly Colour UserColour;
private readonly int UserRank;
private readonly UserPermissions UserPerms;
private readonly string Body;
private readonly bool IsAction;
private readonly bool IsPrivate;
private readonly bool IsBroadcast; // this should be MessageBroadcastLogPacket
private readonly bool Notify;
public MessageAddLogS2CPacket(
long messageId,
DateTimeOffset timeStamp,
long userId,
string userName,
Colour userColour,
int userRank,
UserPermissions userPerms,
string body,
bool isAction,
bool isPrivate,
bool isBroadcast,
bool notify
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserId = userId < 0 ? -1 : userId;
UserName = userName;
UserColour = userColour;
UserRank = userRank;
UserPerms = userPerms;
Body = body;
IsAction = isAction;
IsPrivate = isPrivate;
IsBroadcast = isBroadcast;
Notify = notify;
}
public string Pack() {
string body = SockChatUtility.SanitiseMessageBody(Body);
if(IsAction)
body = string.Format("<i>{0}</i>", body);
if(IsBroadcast)
body = "0\fsay\f" + body;
string userPerms = UserId < 0 ? string.Empty : string.Format(
"{0} {1} {2} {3} {4}",
UserRank,
UserPerms.HasFlag(UserPermissions.KickUser) == true ? 1 : 0,
UserPerms.HasFlag(UserPermissions.ViewLogs) == true ? 1 : 0,
UserPerms.HasFlag(UserPermissions.SetOwnNickname) == true ? 1 : 0,
UserPerms.HasFlag(UserPermissions.CreateChannel) == true ? (
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) == true ? 2 : 1
) : 0
);
return string.Format(
"7\t1\t{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}{9}{10}{11}{12}",
TimeStamp.ToUnixTimeSeconds(),
UserId,
UserName,
UserColour,
userPerms,
body,
MessageId,
Notify ? 1 : 0,
1,
IsAction ? 1 : 0,
0,
IsAction ? 0 : 1,
IsPrivate ? 1 : 0
);
}
}
}

View file

@ -0,0 +1,47 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class MessageAddS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly long UserId;
private readonly string Body;
private readonly bool IsAction;
private readonly bool IsPrivate;
public MessageAddS2CPacket(
long messageId,
DateTimeOffset timeStamp,
long userId,
string body,
bool isAction,
bool isPrivate
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserId = userId < 0 ? -1 : userId;
Body = body;
IsAction = isAction;
IsPrivate = isPrivate;
}
public string Pack() {
string body = SockChatUtility.SanitiseMessageBody(Body);
if(IsAction)
body = string.Format("<i>{0}</i>", body);
return string.Format(
"2\t{0}\t{1}\t{2}\t{3}\t{4}{5}{6}{7}{8}",
TimeStamp.ToUnixTimeSeconds(),
UserId,
body,
MessageId,
1,
IsAction ? 1 : 0,
0,
IsAction ? 0 : 1,
IsPrivate ? 1 : 0
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class MessageBroadcastS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string Body;
public MessageBroadcastS2CPacket(long messageId, DateTimeOffset timeStamp, string body) {
MessageId = messageId;
TimeStamp = timeStamp;
Body = body;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fsay\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseMessageBody(Body),
MessageId
);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class MessageDeleteNotAllowedErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
public MessageDeleteNotAllowedErrorS2CPacket() {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fdelerr\t{1}\t10010",
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,13 @@
namespace SharpChat.SockChat.PacketsS2C {
public class MessageDeleteS2CPacket : ISockChatS2CPacket {
private readonly string DeletedMessageId;
public MessageDeleteS2CPacket(string deletedMessageId) {
DeletedMessageId = deletedMessageId;
}
public string Pack() {
return string.Format("6\t{0}", DeletedMessageId);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class PardonResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string Subject;
public PardonResponseS2CPacket(string subject) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
Subject = subject;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\funban\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
Subject,
MessageId
);
}
}
}

View file

@ -0,0 +1,7 @@
namespace SharpChat.SockChat.PacketsS2C {
public class PongS2CPacket : ISockChatS2CPacket {
public string Pack() {
return "0\tpong";
}
}
}

View file

@ -0,0 +1,16 @@
namespace SharpChat.SockChat.PacketsS2C {
public class UserChannelForceJoinS2CPacket : ISockChatS2CPacket {
private readonly string ChannelName;
public UserChannelForceJoinS2CPacket(string channelName) {
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"5\t2\t{0}",
SockChatUtility.SanitiseChannelName(ChannelName)
);
}
}
}

View file

@ -0,0 +1,28 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserChannelJoinLogS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
public UserChannelJoinLogS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string userName
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserName = userName;
}
public string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fjchan\f{1}\t{2}\t0\t10010",
TimeStamp.ToUnixTimeSeconds(),
UserName,
MessageId
);
}
}
}

View file

@ -0,0 +1,42 @@
namespace SharpChat.SockChat.PacketsS2C {
public class UserChannelJoinS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly long UserId;
private readonly string UserName;
private readonly Colour UserColour;
private readonly int UserRank;
private readonly UserPermissions UserPerms;
public UserChannelJoinS2CPacket(
long userId,
string userName,
Colour userColour,
int userRank,
UserPermissions userPerms
) {
MessageId = SharpId.Next();
UserId = userId;
UserName = userName;
UserColour = userColour;
UserRank = userRank;
UserPerms = userPerms;
}
public string Pack() {
return string.Format(
"5\t0\t{0}\t{1}\t{2}\t{3} {4} {5} {6} {7}\t{8}",
UserId,
UserName,
UserColour,
UserRank,
UserPerms.HasFlag(UserPermissions.KickUser) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.ViewLogs) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.SetOwnNickname) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.CreateChannel) ? (
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
) : 0,
MessageId
);
}
}
}

View file

@ -0,0 +1,28 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserChannelLeaveLogS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
public UserChannelLeaveLogS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string userName
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserName = userName;
}
public string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\flchan\f{1}\t{2}\t0\t10010",
TimeStamp.ToUnixTimeSeconds(),
UserName,
MessageId
);
}
}
}

View file

@ -0,0 +1,19 @@
namespace SharpChat.SockChat.PacketsS2C {
public class UserChannelLeaveS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly long UserId;
public UserChannelLeaveS2CPacket(long userId) {
MessageId = SharpId.Next();
UserId = userId;
}
public string Pack() {
return string.Format(
"5\t1\t{0}\t{1}",
UserId,
MessageId
);
}
}
}

View file

@ -0,0 +1,28 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserConnectLogS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
public UserConnectLogS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string userName
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserName = userName;
}
public string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fjoin\f{1}\t{2}\t0\t10010",
TimeStamp.ToUnixTimeSeconds(),
UserName,
MessageId
);
}
}
}

View file

@ -0,0 +1,49 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserConnectS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly long UserId;
private readonly string UserName;
private readonly Colour UserColour;
private readonly int UserRank;
private readonly UserPermissions UserPerms;
public UserConnectS2CPacket(
long messageId,
DateTimeOffset timeStamp,
long userId,
string userName,
Colour userColour,
int userRank,
UserPermissions userPerms
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserId = userId;
UserName = userName;
UserColour = userColour;
UserRank = userRank;
UserPerms = userPerms;
}
public string Pack() {
return string.Format(
"1\t{0}\t{1}\t{2}\t{3}\t{4} {5} {6} {7} {8}\t{9}",
TimeStamp.ToUnixTimeSeconds(),
UserId,
UserName,
UserColour,
UserRank,
UserPerms.HasFlag(UserPermissions.KickUser) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.ViewLogs) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.SetOwnNickname) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.CreateChannel) ? (
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
) : 0,
MessageId
);
}
}
}

View file

@ -0,0 +1,38 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserDisconnectLogS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
private readonly UserDisconnectReason Reason;
public UserDisconnectLogS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string userName,
UserDisconnectReason reason
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserName = userName;
Reason = reason;
}
public string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\f{1}\f{2}\t{3}\t0\t10010",
TimeStamp.ToUnixTimeSeconds(),
Reason switch {
UserDisconnectReason.Leave => "leave",
UserDisconnectReason.TimeOut => "timeout",
UserDisconnectReason.Kicked => "kick",
UserDisconnectReason.Flood => "flood",
_ => "leave",
},
UserName,
MessageId
);
}
}
}

View file

@ -0,0 +1,42 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserDisconnectS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly long UserId;
private readonly string UserName;
private readonly UserDisconnectReason Reason;
public UserDisconnectS2CPacket(
long messageId,
DateTimeOffset timeStamp,
long userId,
string userName,
UserDisconnectReason reason
) {
MessageId = messageId;
TimeStamp = timeStamp;
UserId = userId;
UserName = userName;
Reason = reason;
}
public string Pack() {
return string.Format(
"3\t{0}\t{1}\t{2}\t{3}\t{4}",
UserId,
UserName,
Reason switch {
UserDisconnectReason.Leave => "leave",
UserDisconnectReason.TimeOut => "timeout",
UserDisconnectReason.Kicked => "kick",
UserDisconnectReason.Flood => "flood",
_ => "leave",
},
TimeStamp.ToUnixTimeSeconds(),
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserNameInUseErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
public UserNameInUseErrorS2CPacket(string userName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
UserName = userName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fnameinuse\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
UserName,
MessageId
);
}
}
}

View file

@ -0,0 +1,32 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserNickChangeLogS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string PrevName;
private readonly string NewName;
public UserNickChangeLogS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string prevName,
string newName
) {
MessageId = messageId;
TimeStamp = timeStamp;
PrevName = prevName;
NewName = newName;
}
public string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fnick\f{1}\f{2}\t{3}\t0\t10010",
TimeStamp.ToUnixTimeSeconds(),
PrevName,
NewName,
MessageId
);
}
}
}

View file

@ -0,0 +1,32 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserNickChangeS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string PrevName;
private readonly string NewName;
public UserNickChangeS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string prevName,
string newName
) {
MessageId = messageId;
TimeStamp = timeStamp;
PrevName = prevName;
NewName = newName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fnick\f{1}\f{2}\t{3}\t10010",
TimeStamp.ToUnixTimeSeconds(),
PrevName,
NewName,
MessageId
);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserNotFoundErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
public UserNotFoundErrorS2CPacket(string userName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
UserName = userName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fusernf\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
UserName,
MessageId
);
}
}
}

View file

@ -0,0 +1,39 @@
namespace SharpChat.SockChat.PacketsS2C {
public class UserUpdateS2CPacket : ISockChatS2CPacket {
private readonly long UserId;
private readonly string UserName;
private readonly Colour UserColour;
private readonly int UserRank;
private readonly UserPermissions UserPerms;
public UserUpdateS2CPacket(
long userId,
string userName,
Colour userColour,
int userRank,
UserPermissions userPerms
) {
UserId = userId;
UserName = userName;
UserColour = userColour;
UserRank = userRank;
UserPerms = userPerms;
}
public string Pack() {
return string.Format(
"10\t{0}\t{1}\t{2}\t{3} {4} {5} {6} {7}",
UserId,
UserName,
UserColour,
UserRank,
UserPerms.HasFlag(UserPermissions.KickUser) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.ViewLogs) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.SetOwnNickname) ? 1 : 0,
UserPerms.HasFlag(UserPermissions.CreateChannel) ? (
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
) : 0
);
}
}
}

View file

@ -0,0 +1,37 @@
using System.Text;
namespace SharpChat.SockChat.PacketsS2C {
public class UsersPopulateS2CPacket : ISockChatS2CPacket {
public record ListEntry(long Id, string Name, Colour Colour, int Rank, UserPermissions Perms, bool Visible);
private readonly ListEntry[] Entries;
public UsersPopulateS2CPacket(ListEntry[] entries) {
Entries = entries;
}
public string Pack() {
StringBuilder sb = new();
sb.AppendFormat("7\t0\t{0}", Entries.Length);
foreach(ListEntry entry in Entries)
sb.AppendFormat(
"\t{0}\t{1}\t{2}\t{3} {4} {5} {6} {7}\t{8}",
entry.Id,
entry.Name,
entry.Colour,
entry.Rank,
entry.Perms.HasFlag(UserPermissions.KickUser) ? 1 : 0,
entry.Perms.HasFlag(UserPermissions.ViewLogs) ? 1 : 0,
entry.Perms.HasFlag(UserPermissions.SetOwnNickname) ? 1 : 0,
entry.Perms.HasFlag(UserPermissions.CreateChannel) ? (
entry.Perms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
) : 0,
entry.Visible ? 1 : 0
);
return sb.ToString();
}
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class WhoChannelNotFoundErrorS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
public WhoChannelNotFoundErrorS2CPacket(string channelName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t1\fwhoerr\f{1}\t{2}\t10010",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName),
MessageId
);
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Text;
namespace SharpChat.SockChat.PacketsS2C {
public class WhoChannelResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string ChannelName;
private readonly string[] Users;
private readonly string SelfName;
public WhoChannelResponseS2CPacket(string channelName, string[] users, string selfName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
ChannelName = channelName;
Users = users;
SelfName = selfName;
}
public string Pack() {
StringBuilder sb = new();
sb.AppendFormat(
"2\t{0}\t-1\t0\fwhochan\f{1}\f",
TimeStamp.ToUnixTimeSeconds(),
SockChatUtility.SanitiseChannelName(ChannelName)
);
foreach(string userName in Users) {
sb.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(userName.Equals(SelfName, StringComparison.InvariantCultureIgnoreCase))
sb.Append(@" style=""font-weight: bold;""");
sb.AppendFormat(@">{0}</a>, ", userName);
}
if(Users.Length > 0)
sb.Length -= 2;
sb.AppendFormat("\t{0}\t10010", MessageId);
return sb.ToString();
}
}
}

View file

@ -0,0 +1,40 @@
using System;
using System.Text;
namespace SharpChat.SockChat.PacketsS2C {
public class WhoServerResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string[] Users;
private readonly string SelfName;
public WhoServerResponseS2CPacket(string[] users, string selfName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
Users = users;
SelfName = selfName;
}
public string Pack() {
StringBuilder sb = new();
sb.AppendFormat("2\t{0}\t-1\t0\fwho\f", TimeStamp.ToUnixTimeSeconds());
foreach(string userName in Users) {
sb.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(userName.Equals(SelfName, StringComparison.InvariantCultureIgnoreCase))
sb.Append(@" style=""font-weight: bold;""");
sb.AppendFormat(@">{0}</a>, ", userName);
}
if(Users.Length > 0)
sb.Length -= 2;
sb.AppendFormat("\t{0}\t10010", MessageId);
return sb.ToString();
}
}
}

View file

@ -0,0 +1,27 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class WhoisResponseS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string UserName;
private readonly string RemoteAddress;
public WhoisResponseS2CPacket(string userName, string remoteAddress) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
UserName = userName;
RemoteAddress = remoteAddress;
}
public string Pack() {
return string.Format(
"2\t{0}\t-1\t0\fipaddr\f{1}\f{2}\t{3}\t10010",
TimeStamp.ToUnixTimeSeconds(),
UserName,
RemoteAddress,
MessageId
);
}
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fleck" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharpChat.Misuzu\SharpChat.Misuzu.csproj" />
<ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,43 @@
using Fleck;
using SharpChat.SockChat.PacketsS2C;
using System.Net;
namespace SharpChat.SockChat {
public class SockChatConnectionInfo : ConnectionInfo {
public IWebSocketConnection Socket { get; }
public SockChatConnectionInfo(
IWebSocketConnection socket,
IPAddress remoteAddr,
ushort remotePort
) : base(remoteAddr, remotePort) {
Socket = socket;
}
public static SockChatConnectionInfo Create(IWebSocketConnection socket) {
IPAddress remoteAddr = IPAddress.Parse(socket.ConnectionInfo.ClientIpAddress);
if(IPAddress.IsLoopback(remoteAddr)
&& socket.ConnectionInfo.Headers.ContainsKey("X-Real-IP")
&& IPAddress.TryParse(socket.ConnectionInfo.Headers["X-Real-IP"], out IPAddress? realAddr))
remoteAddr = realAddr;
return new SockChatConnectionInfo(socket, remoteAddr, (ushort)socket.ConnectionInfo.ClientPort);
}
public void Send(string packet) {
if(Socket.IsAvailable)
Socket.Send(packet).Wait();
}
public void Send(ISockChatS2CPacket packet) {
string data = packet.Pack();
if(!string.IsNullOrWhiteSpace(data))
Send(data);
}
public void Close(int code) {
Socket.Close(code);
}
}
}

View file

@ -0,0 +1,565 @@
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.SockChat;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace SharpChat {
public class SockChatContext : IChatEventHandler {
public readonly SemaphoreSlim ContextAccess = new(1, 1);
public IEventStorage EventStorage { get; }
public ChatEventDispatcher Events { get; } = new();
public ConnectionsContext Connections { get; } = new();
public ChannelsContext Channels { get; } = new();
public UsersContext Users { get; } = new();
public UserStatusContext UserStatuses { get; } = new();
public ChannelsUsersContext ChannelsUsers { get; } = new();
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
public SockChatContext(IEventStorage evtStore) {
EventStorage = evtStore;
Events.Subscribe(evtStore);
Events.Subscribe(this);
}
public void HandleEvent(ChatEventInfo info) {
UserStatusInfo userStatusInfo = UserStatuses.Get(info.SenderId);
if(!string.IsNullOrWhiteSpace(info.ChannelName))
ChannelsUsers.SetUserLastChannel(info.SenderId, info.ChannelName);
// TODO: should user:connect and user:disconnect be channel agnostic?
switch(info.Type) {
case "user:add":
Users.Add(new UserInfo(
info.SenderId,
info.SenderName,
info.SenderColour,
info.SenderRank,
info.SenderPerms,
info.SenderNickName,
info.Data is UserAddEventData uaData && uaData.IsSuper
));
break;
case "user:delete":
UserStatuses.Clear(info.SenderId);
Users.Remove(info.SenderId);
break;
case "user:connect":
if(info.Data is not UserConnectEventData ucData || !ucData.Notify)
break;
SendTo(info.ChannelName, new UserConnectS2CPacket(
info.Id,
info.Created,
info.SenderId,
SockChatUtility.GetUserName(info, userStatusInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
));
ChannelsUsers.Join(info.ChannelName, info.SenderId);
break;
case "user:disconnect":
ChannelInfo[] channels = Channels.GetMany(ChannelsUsers.GetUserChannelNames(info.SenderId));
ChannelsUsers.DeleteUser(info.SenderId);
if(channels.Length > 0) {
UserDisconnectS2CPacket udPacket = new(
info.Id,
info.Created,
info.SenderId,
SockChatUtility.GetUserName(info, userStatusInfo),
info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave
);
foreach(ChannelInfo chan in channels) {
if(chan.IsTemporary && chan.OwnerId == info.SenderId)
Events.Dispatch("chan:delete", chan.Name, info);
else
SendTo(chan, udPacket);
}
}
break;
case "user:status":
if(info.Data is not UserStatusUpdateEventData userStatusUpdate)
break;
if(userStatusInfo.Status == userStatusUpdate.Status
&& userStatusInfo.Text.Equals(userStatusUpdate.Text))
break;
userStatusInfo = UserStatuses.Set(
info.SenderId,
userStatusUpdate.Status,
userStatusUpdate.Text ?? string.Empty
);
SendToUserChannels(info.SenderId, new UserUpdateS2CPacket(
info.SenderId,
SockChatUtility.GetUserName(info, userStatusInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
));
break;
case "user:update":
if(info.Data is not UserUpdateEventData userUpdate)
break;
UserInfo? uuUserInfo = Users.Get(info.SenderId);
if(uuUserInfo is null)
break;
bool uuHasChanged = false;
string? uuPrevName = null;
if(userUpdate.Name != null && !userUpdate.Name.Equals(uuUserInfo.UserName)) {
uuUserInfo.UserName = userUpdate.Name;
uuHasChanged = true;
}
if(userUpdate.NickName != null && !userUpdate.NickName.Equals(uuUserInfo.NickName)) {
if(userUpdate.Notify)
uuPrevName = string.IsNullOrWhiteSpace(uuUserInfo.NickName) ? uuUserInfo.UserName : uuUserInfo.NickName;
uuUserInfo.NickName = userUpdate.NickName;
uuHasChanged = true;
}
if(userUpdate.Colour.HasValue && userUpdate.Colour != uuUserInfo.Colour.ToMisuzu()) {
uuUserInfo.Colour = Colour.FromMisuzu(userUpdate.Colour.Value);
uuHasChanged = true;
}
if(userUpdate.Rank != null && userUpdate.Rank != uuUserInfo.Rank) {
uuUserInfo.Rank = userUpdate.Rank.Value;
uuHasChanged = true;
}
if(userUpdate.Perms.HasValue && userUpdate.Perms != uuUserInfo.Permissions) {
uuUserInfo.Permissions = userUpdate.Perms.Value;
uuHasChanged = true;
}
if(userUpdate.IsSuper.HasValue && userUpdate.IsSuper != uuUserInfo.IsSuper)
uuUserInfo.IsSuper = userUpdate.IsSuper.Value;
if(uuHasChanged) {
if(uuPrevName != null)
SendToUserChannels(info.SenderId, new UserNickChangeS2CPacket(
info.Id,
info.Created,
string.IsNullOrWhiteSpace(info.SenderNickName) ? uuPrevName : $"~{info.SenderNickName}",
SockChatUtility.GetUserName(uuUserInfo, userStatusInfo)
));
SendToUserChannels(info.SenderId, new UserUpdateS2CPacket(
uuUserInfo.UserId,
SockChatUtility.GetUserName(uuUserInfo, userStatusInfo),
uuUserInfo.Colour,
uuUserInfo.Rank,
uuUserInfo.Permissions
));
}
break;
case "user:kickban":
if(info.Data is not UserKickBanEventData userBaka)
break;
SendTo(info.SenderId, new ForceDisconnectS2CPacket(userBaka.Expires));
ConnectionInfo[] conns = Connections.GetUser(info.SenderId);
foreach(ConnectionInfo conn in conns) {
Connections.Remove(conn);
if(conn is SockChatConnectionInfo scConn)
scConn.Close(1000);
Logger.Write($"<{conn.RemoteEndPoint}> Nuked connection from banned user #{conn.UserId}.");
}
string bakaChannelName = ChannelsUsers.GetUserLastChannel(info.SenderId);
if(!string.IsNullOrWhiteSpace(bakaChannelName))
Events.Dispatch(new ChatEventInfo(
SharpId.Next(),
"user:disconnect",
info.Created,
bakaChannelName,
info.SenderId,
info.SenderName,
info.SenderColour,
info.SenderRank,
info.SenderNickName,
info.SenderPerms,
new UserDisconnectEventData(userBaka.Reason)
));
break;
case "chan:join":
HandleUserChannelJoin(
info.ChannelName,
new UserInfo(info), // kinda stinky
userStatusInfo
);
break;
case "chan:leave":
ChannelsUsers.Leave(info.ChannelName, info.SenderId);
SendTo(info.ChannelName, new UserChannelLeaveS2CPacket(info.SenderId));
ChannelInfo? clChannelInfo = Channels.Get(info.ChannelName);
if(clChannelInfo?.IsTemporary == true && clChannelInfo.OwnerId == info.SenderId)
Events.Dispatch("chan:delete", clChannelInfo.Name, info);
break;
case "chan:add":
if(info.Data is not ChannelAddEventData caData)
break;
ChannelInfo caChannelInfo = new(
info.ChannelName,
caData.Password,
caData.IsTemporary,
caData.MinRank,
info.SenderId
);
Channels.Add(caChannelInfo);
foreach(UserInfo ccu in Users.GetMany(minRank: caChannelInfo.Rank))
SendTo(ccu, new ChannelCreateS2CPacket(
caChannelInfo.Name,
caChannelInfo.HasPassword,
caChannelInfo.IsTemporary
));
break;
case "chan:update":
if(info.Data is not ChannelUpdateEventData cuData)
break;
ChannelInfo? cuChannelInfo = Channels.Get(info.ChannelName);
if(cuChannelInfo is null)
break;
string cuChannelName = cuChannelInfo.Name;
if(!string.IsNullOrEmpty(cuData.Name))
cuChannelInfo.Name = cuData.Name;
if(cuData.MinRank.HasValue)
cuChannelInfo.Rank = cuData.MinRank.Value;
if(cuData.Password != null) // this should probably be hashed
cuChannelInfo.Password = cuData.Password;
if(cuData.IsTemporary.HasValue)
cuChannelInfo.IsTemporary = cuData.IsTemporary.Value;
bool nameChanged = !cuChannelName.Equals(cuChannelInfo.Name, StringComparison.InvariantCultureIgnoreCase);
if(nameChanged)
ChannelsUsers.RenameChannel(cuChannelName, cuChannelInfo.Name);
// TODO: Users that no longer have access to the channel/gained access to the channel by the rank change should receive delete and create packets respectively
// the server currently doesn't keep track of what channels a user is already aware of so can't really simulate this yet.
foreach(UserInfo user in Users.GetMany(minRank: cuChannelInfo.Rank))
SendTo(user, new ChannelUpdateS2CPacket(
cuChannelName,
cuChannelInfo.Name,
cuChannelInfo.HasPassword,
cuChannelInfo.IsTemporary
));
if(nameChanged)
SendTo(cuChannelInfo, new UserChannelForceJoinS2CPacket(cuChannelInfo.Name));
break;
case "chan:delete":
ChannelInfo? cdTargetChannelInfo = Channels.Get(info.ChannelName);
ChannelInfo? cdMainChannelInfo = Channels.MainChannel;
if(cdTargetChannelInfo == null || cdMainChannelInfo == null || cdTargetChannelInfo == Channels.MainChannel)
break;
// Remove channel from the listing
Channels.Remove(info.ChannelName);
// Move all users back to the main channel
UserInfo[] cdUserInfos = Users.GetMany(ChannelsUsers.GetChannelUserIds(info.ChannelName));
ChannelsUsers.DeleteChannel(cdMainChannelInfo);
foreach(UserInfo userInfo in cdUserInfos)
HandleUserChannelJoin(
info.ChannelName,
userInfo,
UserStatuses.Get(userInfo)
);
// Broadcast deletion of channel
foreach(UserInfo user in Users.GetMany(minRank: cdTargetChannelInfo.Rank))
SendTo(user, new ChannelDeleteS2CPacket(cdTargetChannelInfo.Name));
break;
case "msg:delete":
if(info.Data is not MessageDeleteEventData msgDelete)
break;
MessageDeleteS2CPacket msgDelPacket = new(msgDelete.MessageId);
if(info.IsBroadcast) {
Send(msgDelPacket);
} else if(info.ChannelName.StartsWith('@')) {
long[] targetIds = info.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray();
if(targetIds.Length != 2)
return;
UserInfo[] users = Users.GetMany(targetIds);
UserInfo? target = users.FirstOrDefault(u => u.UserId != info.SenderId);
if(target == null)
return;
foreach(UserInfo user in users)
SendTo(user, msgDelPacket);
} else {
SendTo(info.ChannelName, msgDelPacket);
}
break;
case "msg:add":
if(info.Data is not MessageAddEventData msgAdd)
break;
if(info.IsBroadcast) {
Send(new MessageBroadcastS2CPacket(info.Id, info.Created, msgAdd.Text));
} else if(info.ChannelName.StartsWith('@')) {
// The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed
// e.g. nook sees @Arysil and Arysil sees @nook
long[] targetIds = info.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray();
if(targetIds.Length != 2)
return;
UserInfo[] users = Users.GetMany(targetIds);
UserInfo? target = users.FirstOrDefault(u => u.UserId != info.SenderId);
if(target == null)
return;
foreach(UserInfo user in users)
SendTo(user, new MessageAddS2CPacket(
info.Id,
DateTimeOffset.Now,
info.SenderId,
info.SenderId == user.UserId ? $"{SockChatUtility.GetUserName(target)} {msgAdd.Text}" : msgAdd.Text,
msgAdd.IsAction,
true
));
} else {
ChannelInfo? channel = Channels.Get(info.ChannelName, SockChatUtility.SanitiseChannelName);
if(channel != null)
SendTo(channel, new MessageAddS2CPacket(
info.Id,
DateTimeOffset.Now,
info.SenderId,
msgAdd.Text,
msgAdd.IsAction,
false
));
}
break;
}
}
private void HandleUserChannelJoin(string channelName, UserInfo userInfo, UserStatusInfo statusInfo) {
SendTo(userInfo.UserId, new ClearMessagesAndUsersS2CPacket());
UserInfo[] userInfos = Users.GetMany(ChannelsUsers.GetChannelUserIds(channelName));
List<UsersPopulateS2CPacket.ListEntry> userEntries = new();
foreach(UserInfo memberInfo in userInfos)
if(memberInfo.UserId != userInfo.UserId)
userEntries.Add(new(
memberInfo.UserId,
SockChatUtility.GetUserName(memberInfo, UserStatuses.Get(memberInfo)),
memberInfo.Colour,
memberInfo.Rank,
memberInfo.Permissions,
true
));
SendTo(userInfo.UserId, new UsersPopulateS2CPacket(userEntries.ToArray()));
SendTo(channelName, new UserChannelJoinS2CPacket(
userInfo.UserId,
SockChatUtility.GetUserName(userInfo, statusInfo),
userInfo.Colour,
userInfo.Rank,
userInfo.Permissions
));
HandleChannelEventLog(channelName, p => SendTo(userInfo.UserId, p));
ChannelsUsers.Join(channelName, userInfo.UserId);
SendTo(userInfo.UserId, new UserChannelForceJoinS2CPacket(channelName));
}
public void Update() {
ConnectionInfo[] timedOut = Connections.GetTimedOut();
foreach(ConnectionInfo conn in timedOut) {
Connections.Remove(conn);
if(conn is SockChatConnectionInfo scConn)
scConn.Close(1002);
Logger.Write($"<{conn.RemoteEndPoint}> Nuked timed out connection from user #{conn.UserId}.");
}
foreach(UserInfo user in Users.All)
if(!Connections.HasUser(user)) {
Events.Dispatch("user:delete", user);
Events.Dispatch(
"user:disconnect",
ChannelsUsers.GetUserLastChannel(user),
user,
new UserDisconnectEventData(UserDisconnectReason.TimeOut)
);
Logger.Write($"Timed out {user} (no more connections).");
}
}
public void SafeUpdate() {
ContextAccess.Wait();
try {
Update();
} finally {
ContextAccess.Release();
}
}
public void HandleChannelEventLog(string channelName, Action<ISockChatS2CPacket> handler) {
foreach(ChatEventInfo info in EventStorage.GetChannelEventLog(channelName)) {
switch(info.Type) {
case "msg:add":
if(info.Data is not MessageAddEventData msgAdd)
break;
handler(new MessageAddLogS2CPacket(
info.Id,
info.Created,
info.SenderId,
info.SenderName,
info.SenderColour,
info.SenderRank,
info.SenderPerms,
msgAdd.Text,
msgAdd.IsAction,
info.ChannelName.StartsWith('@'),
info.IsBroadcast,
false
));
break;
case "user:connect":
if(info.Data is UserConnectEventData ucData && ucData.Notify)
handler(new UserConnectLogS2CPacket(
info.Id,
info.Created,
SockChatUtility.GetUserName(info)
));
break;
case "user:disconnect":
handler(new UserDisconnectLogS2CPacket(
info.Id,
info.Created,
SockChatUtility.GetUserName(info),
info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave
));
break;
case "user:update":
if(info.Data is UserUpdateEventData userUpdate && userUpdate.Notify)
handler(new UserNickChangeLogS2CPacket(
info.Id,
info.Created,
info.SenderNickName == null ? info.SenderName : $"~{info.SenderNickName}",
userUpdate.NickName == null ? info.SenderName : $"~{userUpdate.NickName}"
));
break;
case "chan:join":
handler(new UserChannelJoinLogS2CPacket(
info.Id,
info.Created,
SockChatUtility.GetUserName(info)
));
break;
case "chan:leave":
handler(new UserChannelLeaveLogS2CPacket(
info.Id,
info.Created,
SockChatUtility.GetUserName(info)
));
break;
}
}
}
public void Send(ISockChatS2CPacket packet) {
string data = packet.Pack();
Connections.WithAuthed(conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Send(data);
});
}
public void SendTo(UserInfo user, ISockChatS2CPacket packet) {
SendTo(user.UserId, packet.Pack());
}
public void SendTo(long userId, ISockChatS2CPacket packet) {
SendTo(userId, packet.Pack());
}
public void SendTo(long userId, string packet) {
Connections.WithUser(userId, conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Send(packet);
});
}
public void SendTo(ChannelInfo channel, ISockChatS2CPacket packet) {
SendTo(channel.Name, packet.Pack());
}
public void SendTo(ChannelInfo channel, string packet) {
SendTo(channel.Name, packet);
}
public void SendTo(string channelName, ISockChatS2CPacket packet) {
SendTo(channelName, packet.Pack());
}
public void SendTo(string channelName, string packet) {
long[] userIds = ChannelsUsers.GetChannelUserIds(channelName);
foreach(long userId in userIds)
Connections.WithUser(userId, conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Send(packet);
});
}
public void SendToUserChannels(long userId, ISockChatS2CPacket packet) {
ChannelInfo[] chans = Channels.GetMany(ChannelsUsers.GetUserChannelNames(userId));
string data = packet.Pack();
foreach(ChannelInfo chan in chans)
SendTo(chan, data);
}
}
}

View file

@ -0,0 +1,265 @@
using Fleck;
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Misuzu;
using SharpChat.SockChat.Commands;
using SharpChat.SockChat.PacketsC2S;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
namespace SharpChat.SockChat {
public class SockChatServer : IDisposable {
public const ushort DEFAULT_PORT = 6770;
public const int DEFAULT_MSG_LENGTH_MAX = 5000;
public const int DEFAULT_MAX_CONNECTIONS = 5;
public const int DEFAULT_FLOOD_KICK_LENGTH = 30;
public IWebSocketServer Server { get; }
public SockChatContext Context { get; }
private readonly HttpClient HttpClient;
private readonly MisuzuClient Misuzu;
private readonly CachedValue<int> MaxMessageLength;
private readonly CachedValue<int> MaxConnections;
private readonly CachedValue<int> FloodKickLength;
private readonly List<IC2SPacketHandler> GuestHandlers = new();
private readonly List<IC2SPacketHandler> AuthedHandlers = new();
private readonly SendMessageC2SPacketHandler SendMessageHandler;
private bool IsShuttingDown = false;
private bool IsRestarting = false;
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IEventStorage evtStore, IConfig config) {
Logger.Write("Initialising Sock Chat server...");
DateTimeOffset started = DateTimeOffset.UtcNow;
HttpClient = httpClient;
Misuzu = msz;
MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
Context = new SockChatContext(evtStore);
string[]? channelNames = config.ReadValue("channels", new[] { "lounge" });
if(channelNames != null)
foreach(string channelName in channelNames) {
IConfig channelCfg = config.ScopeTo($"channels:{channelName}");
string? name = channelCfg.SafeReadValue("name", string.Empty);
if(string.IsNullOrWhiteSpace(name))
name = channelName;
ChannelInfo channelInfo = new(
name,
channelCfg.SafeReadValue("password", string.Empty),
rank: channelCfg.SafeReadValue("minRank", 0)
);
Context.Channels.Add(channelInfo);
}
if(Context.Channels.PublicCount < 1)
Context.Channels.Add(new ChannelInfo("Default"));
CachedValue<string> motdHeaderFormat = config.ReadCached("motd", @"Welcome to Flashii Chat, {0}!");
GuestHandlers.Add(new AuthC2SPacketHandler(
started,
Misuzu,
Context.Channels.MainChannel,
motdHeaderFormat,
MaxMessageLength,
MaxConnections
));
AuthedHandlers.AddRange(new IC2SPacketHandler[] {
new PingC2SPacketHandler(Misuzu),
SendMessageHandler = new SendMessageC2SPacketHandler(MaxMessageLength),
});
SendMessageHandler.AddCommands(new ISockChatClientCommand[] {
new UserAFKCommand(),
new UserNickCommand(),
new MessageWhisperCommand(),
new MessageActionCommand(),
new WhoCommand(),
new ChannelJoinCommand(),
new ChannelCreateCommand(),
new ChannelDeleteCommand(),
new ChannelPasswordCommand(),
new ChannelRankCommand(),
new MessageBroadcastCommand(),
new MessageDeleteCommand(),
new KickBanCommand(msz),
new PardonUserCommand(msz),
new PardonAddressCommand(msz),
new BanListCommand(msz),
new WhoisCommand(),
});
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
Server = new SockChatWebSocketServer($"ws://0.0.0.0:{port}");
}
public void Listen(ManualResetEvent waitHandle) {
if(waitHandle != null)
SendMessageHandler.AddCommand(new ShutdownRestartCommand(
waitHandle,
() => IsShuttingDown,
restarting => {
IsShuttingDown = true;
IsRestarting = restarting;
}
));
Server.Start(sock => {
if(IsShuttingDown) {
sock.Close(1013);
return;
}
SockChatConnectionInfo conn = SockChatConnectionInfo.Create(sock);
Context.Connections.Add(conn);
sock.OnOpen = () => OnOpen(conn);
sock.OnClose = () => OnClose(conn);
sock.OnError = err => OnError(conn, err);
sock.OnMessage = msg => OnMessage(conn, msg);
});
Logger.Write("Listening...");
}
private void OnOpen(ConnectionInfo conn) {
Logger.Write($"Connection opened from {conn.RemoteEndPoint}");
Context.SafeUpdate();
}
private void OnError(ConnectionInfo conn, Exception ex) {
Logger.Write($"<{conn.RemoteEndPoint}> {ex}");
Context.SafeUpdate();
}
private void OnClose(ConnectionInfo conn) {
Logger.Write($"Connection closed from {conn.RemoteEndPoint}");
Context.ContextAccess.Wait();
try {
Context.Connections.Remove(conn);
if(!Context.Connections.HasUser(conn.UserId)) {
UserInfo? userInfo = Context.Users.Get(conn.UserId);
if(userInfo != null) {
Context.Events.Dispatch("user:delete", userInfo);
Context.Events.Dispatch(
"user:disconnect",
Context.ChannelsUsers.GetUserLastChannel(conn.UserId),
userInfo,
new UserDisconnectEventData(UserDisconnectReason.Leave)
);
}
}
Context.Update();
} finally {
Context.ContextAccess.Release();
}
}
private void OnMessage(SockChatConnectionInfo conn, string msg) {
Context.SafeUpdate();
// this doesn't affect non-authed connections?????
if(conn.UserId > 0) {
long banUserId = 0;
string banAddr = string.Empty;
TimeSpan banDuration = TimeSpan.MinValue;
Context.ContextAccess.Wait();
try {
if(!Context.UserRateLimiters.TryGetValue(conn.UserId, out RateLimiter? rateLimiter))
Context.UserRateLimiters.Add(conn.UserId, rateLimiter = new RateLimiter(
UserInfo.DEFAULT_SIZE,
UserInfo.DEFAULT_MINIMUM_DELAY,
UserInfo.DEFAULT_RISKY_OFFSET
));
rateLimiter.Update();
if(rateLimiter.IsExceeded) {
banDuration = TimeSpan.FromSeconds(FloodKickLength);
banUserId = conn.UserId;
banAddr = conn.RemoteAddress;
} else if(rateLimiter.IsRisky) {
banUserId = conn.UserId;
}
if(banUserId > 0) {
UserInfo? userInfo = Context.Users.Get(banUserId);
if(userInfo != null) {
if(banDuration == TimeSpan.MinValue) {
Context.SendTo(userInfo, new FloodWarningS2CPacket());
} else {
Context.Events.Dispatch("user:kickban", userInfo, UserKickBanEventData.OfDuration(UserDisconnectReason.Flood, banDuration));
if(banDuration > TimeSpan.Zero)
Misuzu.CreateBanAsync(
banUserId.ToString(), conn.RemoteAddress,
string.Empty, "::1",
banDuration,
"Kicked from chat for flood protection."
).Wait();
return;
}
}
}
} finally {
Context.ContextAccess.Release();
}
}
C2SPacketHandlerContext context = new(msg, Context, conn);
IC2SPacketHandler? handler = conn.UserId > 0
? AuthedHandlers.FirstOrDefault(h => h.IsMatch(context))
: GuestHandlers.FirstOrDefault(h => h.IsMatch(context));
handler?.Handle(context);
}
private bool IsDisposed;
~SockChatServer() {
DoDispose();
}
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
IsShuttingDown = true;
Context.Connections.WithAll(conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Close(IsRestarting ? 1012 : 1001);
});
Server?.Dispose();
HttpClient?.Dispose();
}
}
}

View file

@ -0,0 +1,75 @@
using SharpChat.Events;
using System;
using System.Text.RegularExpressions;
namespace SharpChat.SockChat {
public static class SockChatUtility {
private static readonly Regex ChannelName = new(@"[^A-Za-z0-9\-_]", RegexOptions.CultureInvariant | RegexOptions.Compiled);
public static string SanitiseMessageBody(string? body) {
if(string.IsNullOrEmpty(body))
return string.Empty;
return body.Replace("<", "&lt;").Replace(">", "&gt;").Replace("\n", " <br/> ").Replace("\t", " ");
}
public static string SanitiseChannelName(string name) {
return ChannelName.Replace(name.Replace(" ", "_"), "-");
}
public static bool CheckChannelName(string name) {
return name.Length < 1 || ChannelName.IsMatch(name);
}
public static string GetUserName(UserInfo info, UserStatusInfo? statusInfo = null) {
string name = string.IsNullOrWhiteSpace(info.NickName) ? info.UserName : $"~{info.NickName}";
if(statusInfo?.Status == UserStatus.Away)
name = string.Format(
"&lt;{0}&gt;_{1}",
statusInfo.Text[..Math.Min(statusInfo.Text.Length, 5)].ToUpperInvariant(),
name
);
return name;
}
public static string GetUserName(ChatEventInfo info, UserStatusInfo? statusInfo = null) {
string name = string.IsNullOrWhiteSpace(info.SenderNickName) ? info.SenderName : $"~{info.SenderNickName}";
if(statusInfo?.Status == UserStatus.Away)
name = string.Format(
"&lt;{0}&gt;_{1}",
statusInfo.Text[..Math.Min(statusInfo.Text.Length, 5)].ToUpperInvariant(),
name
);
return name;
}
public static (string, UsersContext.NameTarget) ExplodeUserName(string name) {
UsersContext.NameTarget target = UsersContext.NameTarget.UserName;
if(name.StartsWith("<")) {
int gt = name.IndexOf(">_");
if(gt > 0) {
gt += 2;
name = name[gt..];
}
} else if(name.StartsWith("&lt;")) {
int gt = name.IndexOf("&gt;_");
if(gt > 0) {
gt += 5;
name = name[gt..];
}
}
if(name.StartsWith("~")) {
target = UsersContext.NameTarget.NickName;
name = name[1..];
}
return (name, target);
}
}
}

View file

@ -1,4 +1,6 @@
using Fleck;
#nullable disable
using Fleck;
using System;
using System.Collections.Generic;
using System.IO;
@ -14,13 +16,13 @@ using System.Text;
// https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs
namespace SharpChat {
public class SharpChatWebSocketServer : IWebSocketServer {
public class SockChatWebSocketServer : IWebSocketServer {
private readonly string _scheme;
private readonly IPAddress _locationIP;
private Action<IWebSocketConnection> _config;
public SharpChatWebSocketServer(string location, bool supportDualStack = true) {
public SockChatWebSocketServer(string location, bool supportDualStack = true) {
Uri uri = new(location);
Port = uri.Port;
@ -115,7 +117,8 @@ namespace SharpChat {
}
private void OnClientConnect(ISocket clientSocket) {
if(clientSocket == null) return; // socket closed
if(clientSocket == null)
return; // socket closed
FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString()));
ListenForClients();

View file

@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\Shar
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DF7A7073-A67A-4D93-92C6-F9D0F95E2359}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
LICENSE = LICENSE
@ -15,6 +16,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
start.sh = start.sh
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChatCommon", "SharpChatCommon\SharpChatCommon.csproj", "{B2228E3C-E0DB-4AAF-A603-2A822B531F76}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.SockChat", "SharpChat.SockChat\SharpChat.SockChat.csproj", "{4D48CCFB-5D3B-4AB6-AF94-04377474078C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.Misuzu", "SharpChat.Misuzu\SharpChat.Misuzu.csproj", "{08FD8B99-011A-43F9-A6C9-A3C1979604CF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -25,6 +32,18 @@ Global
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.Build.0 = Release|Any CPU
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Release|Any CPU.Build.0 = Release|Any CPU
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Release|Any CPU.Build.0 = Release|Any CPU
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,72 +0,0 @@
using System;
using System.Linq;
using System.Text;
namespace SharpChat {
public class ChatChannel {
public string Name { get; }
public string Password { get; set; }
public bool IsTemporary { get; set; }
public int Rank { get; set; }
public long OwnerId { get; set; }
public bool HasPassword
=> !string.IsNullOrWhiteSpace(Password);
public ChatChannel(
ChatUser owner,
string name,
string password = null,
bool isTemporary = false,
int rank = 0
) : this(name, password, isTemporary, rank, owner?.UserId ?? 0) {}
public ChatChannel(
string name,
string password = null,
bool isTemporary = false,
int rank = 0,
long ownerId = 0
) {
Name = name;
Password = password ?? string.Empty;
IsTemporary = isTemporary;
Rank = rank;
OwnerId = ownerId;
}
public string Pack() {
StringBuilder sb = new();
sb.Append(Name);
sb.Append('\t');
sb.Append(string.IsNullOrEmpty(Password) ? '0' : '1');
sb.Append('\t');
sb.Append(IsTemporary ? '1' : '0');
return sb.ToString();
}
public bool NameEquals(string name) {
return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase);
}
public bool IsOwner(ChatUser user) {
return OwnerId > 0
&& user != null
&& OwnerId == user.UserId;
}
public override int GetHashCode() {
return Name.GetHashCode();
}
public static bool CheckName(string name) {
return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar);
}
public static bool CheckNameChar(char c) {
return char.IsLetter(c) || char.IsNumber(c) || c == '-' || c == '_';
}
}
}

View file

@ -1,79 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace SharpChat {
public struct ChatColour {
public byte Red { get; }
public byte Green { get; }
public byte Blue { get; }
public bool Inherits { get; }
public static ChatColour None { get; } = new();
public ChatColour() {
Red = 0;
Green = 0;
Blue = 0;
Inherits = true;
}
public ChatColour(byte red, byte green, byte blue) {
Red = red;
Green = green;
Blue = blue;
Inherits = false;
}
public override bool Equals([NotNullWhen(true)] object obj) {
return obj is ChatColour colour && Equals(colour);
}
public bool Equals(ChatColour other) {
return Red == other.Red
&& Green == other.Green
&& Blue == other.Blue
&& Inherits == other.Inherits;
}
public override int GetHashCode() {
return ToMisuzu();
}
public override string ToString() {
return Inherits
? "inherit"
: string.Format("#{0:x2}{1:x2}{2:x2}", Red, Green, Blue);
}
public int ToRawRGB() {
return (Red << 16) | (Green << 8) | Blue;
}
public static ChatColour FromRawRGB(int rgb) {
return new(
(byte)((rgb >> 16) & 0xFF),
(byte)((rgb >> 8) & 0xFF),
(byte)(rgb & 0xFF)
);
}
private const int MSZ_INHERIT = 0x40000000;
public int ToMisuzu() {
return Inherits ? MSZ_INHERIT : ToRawRGB();
}
public static ChatColour FromMisuzu(int raw) {
return (raw & MSZ_INHERIT) > 0
? None
: FromRawRGB(raw);
}
public static bool operator ==(ChatColour left, ChatColour right) {
return left.Equals(right);
}
public static bool operator !=(ChatColour left, ChatColour right) {
return !(left == right);
}
}
}

View file

@ -1,53 +0,0 @@
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 ChatConnection Connection { get; }
public ChatChannel Channel { get; }
public ChatCommandContext(
string text,
ChatContext chat,
ChatUser user,
ChatConnection connection,
ChatChannel channel
) {
if(text == null)
throw new ArgumentNullException(nameof(text));
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
User = user ?? throw new ArgumentNullException(nameof(user));
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
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,
ChatConnection connection,
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));
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
}
public bool NameEquals(string name) {
return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View file

@ -1,96 +0,0 @@
using Fleck;
using System;
using System.Collections.Generic;
using System.Net;
namespace SharpChat {
public class ChatConnection : IDisposable {
public const int ID_LENGTH = 20;
#if DEBUG
public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(1);
#else
public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(5);
#endif
public IWebSocketConnection Socket { get; }
public string Id { get; }
public bool IsDisposed { get; private set; }
public DateTimeOffset LastPing { get; set; } = DateTimeOffset.Now;
public ChatUser User { get; set; }
private int CloseCode { get; set; } = 1000;
public IPAddress RemoteAddress { get; }
public ushort RemotePort { get; }
public bool IsAlive => !IsDisposed && !HasTimedOut;
public bool IsAuthed => IsAlive && User is not null;
public ChatConnection(IWebSocketConnection sock) {
Socket = sock;
Id = RNG.SecureRandomString(ID_LENGTH);
if(!IPAddress.TryParse(sock.ConnectionInfo.ClientIpAddress, out IPAddress addr))
throw new Exception("Unable to parse remote address?????");
if(IPAddress.IsLoopback(addr)
&& sock.ConnectionInfo.Headers.ContainsKey("X-Real-IP")
&& IPAddress.TryParse(sock.ConnectionInfo.Headers["X-Real-IP"], out IPAddress realAddr))
addr = realAddr;
RemoteAddress = addr;
RemotePort = (ushort)sock.ConnectionInfo.ClientPort;
}
public void Send(IServerPacket packet) {
if(!Socket.IsAvailable)
return;
IEnumerable<string> data = packet.Pack();
if(data != null)
foreach(string line in data)
if(!string.IsNullOrWhiteSpace(line))
Socket.Send(line);
}
public void BumpPing() {
LastPing = DateTimeOffset.Now;
}
public bool HasTimedOut
=> DateTimeOffset.Now - LastPing > SessionTimeOut;
public void PrepareForRestart() {
CloseCode = 1012;
}
~ChatConnection() {
DoDispose();
}
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Socket.Close(CloseCode);
}
public override string ToString() {
return Id;
}
public override int GetHashCode() {
return Id.GetHashCode();
}
}
}

View file

@ -1,398 +0,0 @@
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Packet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
namespace SharpChat {
public class ChatContext {
public record ChannelUserAssoc(long UserId, string ChannelName);
public readonly SemaphoreSlim ContextAccess = new(1, 1);
public HashSet<ChatChannel> Channels { get; } = new();
public HashSet<ChatConnection> Connections { get; } = new();
public HashSet<ChatUser> Users { get; } = new();
public IEventStorage Events { get; }
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = new();
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
public Dictionary<long, ChatChannel> UserLastChannel { get; } = new();
public ChatContext(IEventStorage evtStore) {
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
}
public void DispatchEvent(IChatEvent eventInfo) {
if(eventInfo is MessageCreateEvent mce) {
if(mce.IsBroadcast) {
Send(new LegacyCommandResponse(LCR.BROADCAST, false, mce.MessageText));
} else if(mce.IsPrivate) {
// The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed
// e.g. nook sees @Arysil and Arysil sees @nook
// this entire routine is garbage, channels should probably in the db
if(!mce.ChannelName.StartsWith("@"))
return;
IEnumerable<long> uids = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1);
if(uids.Count() != 2)
return;
IEnumerable<ChatUser> users = Users.Where(u => uids.Any(uid => uid == u.UserId));
ChatUser target = users.FirstOrDefault(u => u.UserId != mce.SenderId);
if(target == null)
return;
foreach(ChatUser user in users)
SendTo(user, new ChatMessageAddPacket(
mce.MessageId,
DateTimeOffset.Now,
mce.SenderId,
mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText,
mce.IsAction,
true
));
} else {
ChatChannel channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
SendTo(channel, new ChatMessageAddPacket(
mce.MessageId,
DateTimeOffset.Now,
mce.SenderId,
mce.MessageText,
mce.IsAction,
false
));
}
Events.AddEvent(
mce.MessageId, "msg:add",
mce.ChannelName,
mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms,
new { text = mce.MessageText },
(mce.IsBroadcast ? StoredEventFlags.Broadcast : 0)
| (mce.IsAction ? StoredEventFlags.Action : 0)
| (mce.IsPrivate ? StoredEventFlags.Private : 0)
);
return;
}
}
public void Update() {
foreach(ChatConnection conn in Connections)
if(!conn.IsDisposed && conn.HasTimedOut) {
conn.Dispose();
Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
}
Connections.RemoveWhere(conn => conn.IsDisposed);
foreach(ChatUser user in Users)
if(!Connections.Any(conn => conn.User == user)) {
HandleDisconnect(user, UserDisconnectReason.TimeOut);
Logger.Write($"Timed out {user} (no more connections).");
}
}
public void SafeUpdate() {
ContextAccess.Wait();
try {
Update();
} finally {
ContextAccess.Release();
}
}
public bool IsInChannel(ChatUser user, ChatChannel channel) {
return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name));
}
public string[] GetUserChannelNames(ChatUser user) {
return ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName).ToArray();
}
public ChatChannel[] GetUserChannels(ChatUser user) {
string[] names = GetUserChannelNames(user);
return Channels.Where(c => names.Any(n => c.NameEquals(n))).ToArray();
}
public long[] GetChannelUserIds(ChatChannel channel) {
return ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId).ToArray();
}
public ChatUser[] GetChannelUsers(ChatChannel channel) {
long[] ids = GetChannelUserIds(channel);
return Users.Where(u => ids.Contains(u.UserId)).ToArray();
}
public void UpdateUser(
ChatUser user,
string userName = null,
string nickName = null,
ChatColour? colour = null,
ChatUserStatus? status = null,
string statusText = null,
int? rank = null,
ChatUserPermissions? perms = null,
bool? isSuper = null,
bool silent = false
) {
if(user == null)
throw new ArgumentNullException(nameof(user));
bool hasChanged = false;
string previousName = null;
if(userName != null && !user.UserName.Equals(userName)) {
user.UserName = userName;
hasChanged = true;
}
if(nickName != null && !user.NickName.Equals(nickName)) {
if(!silent)
previousName = string.IsNullOrWhiteSpace(user.NickName) ? user.UserName : user.NickName;
user.NickName = nickName;
hasChanged = true;
}
if(colour.HasValue && user.Colour != colour.Value) {
user.Colour = colour.Value;
hasChanged = true;
}
if(status.HasValue && user.Status != status.Value) {
user.Status = status.Value;
hasChanged = true;
}
if(statusText != null && !user.StatusText.Equals(statusText)) {
user.StatusText = statusText;
hasChanged = true;
}
if(rank != null && user.Rank != rank) {
user.Rank = (int)rank;
hasChanged = true;
}
if(perms.HasValue && user.Permissions != perms) {
user.Permissions = perms.Value;
hasChanged = true;
}
if(isSuper.HasValue) {
user.IsSuper = isSuper.Value;
hasChanged = true;
}
if(hasChanged)
SendToUserChannels(user, new UserUpdatePacket(user, previousName));
}
public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(duration > TimeSpan.Zero) {
DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
SendTo(user, new ForceDisconnectPacket(ForceDisconnectReason.Banned, expires));
} else
SendTo(user, new ForceDisconnectPacket(ForceDisconnectReason.Kicked));
foreach(ChatConnection conn in Connections)
if(conn.User == user)
conn.Dispose();
Connections.RemoveWhere(conn => conn.IsDisposed);
HandleDisconnect(user, reason);
}
public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) {
if(!IsInChannel(user, chan)) {
SendTo(chan, new UserConnectPacket(DateTimeOffset.Now, user));
Events.AddEvent("user:connect", user, chan, flags: StoredEventFlags.Log);
}
conn.Send(new AuthSuccessPacket(user, chan, conn, maxMsgLength));
conn.Send(new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank)));
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
conn.Send(new ContextMessagePacket(msg));
conn.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank)));
Users.Add(user);
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
UserLastChannel[user.UserId] = chan;
}
public void HandleDisconnect(ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
UpdateUser(user, status: ChatUserStatus.Offline);
Users.Remove(user);
UserLastChannel.Remove(user.UserId);
ChatChannel[] channels = GetUserChannels(user);
foreach(ChatChannel chan in channels) {
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));
SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason));
Events.AddEvent("user:disconnect", user, chan, new { reason = (int)reason }, StoredEventFlags.Log);
if(chan.IsTemporary && chan.IsOwner(user))
RemoveChannel(chan);
}
}
public void SwitchChannel(ChatUser user, ChatChannel chan, string password) {
if(UserLastChannel.TryGetValue(user.UserId, out ChatChannel ulc) && chan == ulc) {
ForceChannel(user);
return;
}
if(!user.Can(ChatUserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
if(chan.Rank > user.Rank) {
SendTo(user, new LegacyCommandResponse(LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name));
ForceChannel(user);
return;
}
if(!string.IsNullOrEmpty(chan.Password) && chan.Password != password) {
SendTo(user, new LegacyCommandResponse(LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name));
ForceChannel(user);
return;
}
}
ForceChannelSwitch(user, chan);
}
public void ForceChannelSwitch(ChatUser user, ChatChannel chan) {
if(!Channels.Contains(chan))
return;
ChatChannel oldChan = UserLastChannel[user.UserId];
SendTo(oldChan, new UserChannelLeavePacket(user));
Events.AddEvent("chan:leave", user, oldChan, flags: StoredEventFlags.Log);
SendTo(chan, new UserChannelJoinPacket(user));
Events.AddEvent("chan:join", user, oldChan, flags: StoredEventFlags.Log);
SendTo(user, new ContextClearPacket(chan, ContextClearMode.MessagesUsers));
SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank)));
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
SendTo(user, new ContextMessagePacket(msg));
ForceChannel(user, chan);
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name));
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
UserLastChannel[user.UserId] = chan;
if(oldChan.IsTemporary && oldChan.IsOwner(user))
RemoveChannel(oldChan);
}
public void Send(IServerPacket packet) {
if(packet == null)
throw new ArgumentNullException(nameof(packet));
foreach(ChatConnection conn in Connections)
if(conn.IsAuthed)
conn.Send(packet);
}
public void SendTo(ChatUser user, IServerPacket packet) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
foreach(ChatConnection conn in Connections)
if(conn.IsAlive && conn.User == user)
conn.Send(packet);
}
public void SendTo(ChatChannel channel, IServerPacket packet) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
// might be faster to grab the users first and then cascade into that SendTo
IEnumerable<ChatConnection> conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel));
foreach(ChatConnection conn in conns)
conn.Send(packet);
}
public void SendToUserChannels(ChatUser user, IServerPacket packet) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
IEnumerable<ChatChannel> chans = Channels.Where(c => IsInChannel(user, c));
IEnumerable<ChatConnection> conns = Connections.Where(conn => conn.IsAuthed && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName))));
foreach(ChatConnection conn in conns)
conn.Send(packet);
}
public IPAddress[] GetRemoteAddresses(ChatUser user) {
return Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct().ToArray();
}
public void ForceChannel(ChatUser user, ChatChannel chan = null) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
throw new ArgumentException("no channel???");
SendTo(user, new UserChannelForceJoinPacket(chan));
}
public void UpdateChannel(ChatChannel channel, bool? temporary = null, int? hierarchy = null, string password = null) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(!Channels.Contains(channel))
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
if(temporary.HasValue)
channel.IsTemporary = temporary.Value;
if(hierarchy.HasValue)
channel.Rank = hierarchy.Value;
if(password != null)
channel.Password = password;
// TODO: Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) {
SendTo(user, new ChannelUpdatePacket(channel.Name, channel));
}
}
public void RemoveChannel(ChatChannel channel) {
if(channel == null || !Channels.Any())
return;
ChatChannel defaultChannel = Channels.FirstOrDefault();
if(defaultChannel == null)
return;
// Remove channel from the listing
Channels.Remove(channel);
// Move all users back to the main channel
// TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel.
foreach(ChatUser user in GetChannelUsers(channel))
SwitchChannel(user, defaultChannel, string.Empty);
// Broadcast deletion of channel
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank))
SendTo(user, new ChannelDeletePacket(channel));
}
}
}

View file

@ -1,27 +0,0 @@
using System;
namespace SharpChat {
public class ChatPacketHandlerContext {
public string Text { get; }
public ChatContext Chat { get; }
public ChatConnection Connection { get; }
public ChatPacketHandlerContext(
string text,
ChatContext chat,
ChatConnection connection
) {
Text = text ?? throw new ArgumentNullException(nameof(text));
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
}
public bool CheckPacketId(string packetId) {
return Text == packetId || Text.StartsWith(packetId + '\t');
}
public string[] SplitText(int expect) {
return Text.Split('\t', expect + 1);
}
}
}

View file

@ -1,110 +0,0 @@
using System;
using System.Text;
namespace SharpChat {
public class ChatUser : IEquatable<ChatUser> {
public const int DEFAULT_SIZE = 30;
public const int DEFAULT_MINIMUM_DELAY = 10000;
public const int DEFAULT_RISKY_OFFSET = 5;
public long UserId { get; }
public string UserName { get; set; }
public ChatColour Colour { get; set; }
public int Rank { get; set; }
public ChatUserPermissions Permissions { get; set; }
public bool IsSuper { get; set; }
public string NickName { get; set; }
public ChatUserStatus Status { get; set; }
public string StatusText { get; set; }
public string LegacyName => string.IsNullOrWhiteSpace(NickName) ? UserName : $"~{NickName}";
public string LegacyNameWithStatus {
get {
StringBuilder sb = new();
if(Status == ChatUserStatus.Away)
sb.AppendFormat("&lt;{0}&gt;_", StatusText[..Math.Min(StatusText.Length, 5)].ToUpperInvariant());
sb.Append(LegacyName);
return sb.ToString();
}
}
public ChatUser(
long userId,
string userName,
ChatColour colour,
int rank,
ChatUserPermissions perms,
string nickName = null,
ChatUserStatus status = ChatUserStatus.Online,
string statusText = null,
bool isSuper = false
) {
UserId = userId;
UserName = userName ?? throw new ArgumentNullException(nameof(userName));
Colour = colour;
Rank = rank;
Permissions = perms;
NickName = nickName ?? string.Empty;
Status = status;
StatusText = statusText ?? string.Empty;
}
public bool Can(ChatUserPermissions perm, bool strict = false) {
ChatUserPermissions perms = Permissions & perm;
return strict ? perms == perm : perms > 0;
}
public string Pack() {
StringBuilder sb = new();
sb.Append(UserId);
sb.Append('\t');
sb.Append(LegacyNameWithStatus);
sb.Append('\t');
sb.Append(Colour);
sb.Append('\t');
sb.Append(Rank);
sb.Append(' ');
sb.Append(Can(ChatUserPermissions.KickUser) ? '1' : '0');
sb.Append(' ');
sb.Append(Can(ChatUserPermissions.ViewLogs) ? '1' : '0');
sb.Append(' ');
sb.Append(Can(ChatUserPermissions.SetOwnNickname) ? '1' : '0');
sb.Append(' ');
sb.Append(Can(ChatUserPermissions.CreateChannel | ChatUserPermissions.SetChannelPermanent, true) ? '2' : (
Can(ChatUserPermissions.CreateChannel) ? '1' : '0'
));
return sb.ToString();
}
public bool NameEquals(string name) {
return string.Equals(name, UserName, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, NickName, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, LegacyName, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, LegacyNameWithStatus, StringComparison.InvariantCultureIgnoreCase);
}
public override int GetHashCode() {
return UserId.GetHashCode();
}
public override bool Equals(object obj) {
return Equals(obj as ChatUser);
}
public bool Equals(ChatUser other) {
return UserId == other?.UserId;
}
public static string GetDMChannelName(ChatUser user1, ChatUser user2) {
return user1.UserId < user2.UserId
? $"@{user1.UserId}-{user2.UserId}"
: $"@{user2.UserId}-{user1.UserId}";
}
}
}

View file

@ -1,26 +0,0 @@
using System;
namespace SharpChat {
[Flags]
public enum ChatUserPermissions : int {
KickUser = 0x00000001,
BanUser = 0x00000002,
//SilenceUser = 0x00000004,
Broadcast = 0x00000008,
SetOwnNickname = 0x00000010,
SetOthersNickname = 0x00000020,
CreateChannel = 0x00000040,
DeleteChannel = 0x00010000,
SetChannelPermanent = 0x00000080,
SetChannelPassword = 0x00000100,
SetChannelHierarchy = 0x00000200,
JoinAnyChannel = 0x00020000,
SendMessage = 0x00000400,
DeleteOwnMessage = 0x00000800,
DeleteAnyMessage = 0x00001000,
EditOwnMessage = 0x00002000,
EditAnyMessage = 0x00004000,
SeeIPAddress = 0x00008000,
ViewLogs = 0x00040000,
}
}

View file

@ -1,7 +0,0 @@
namespace SharpChat {
public enum ChatUserStatus {
Online,
Away,
Offline,
}
}

Some files were not shown because too many files have changed in this diff Show more