Compare commits
40 commits
mistress
...
new-master
Author | SHA1 | Date | |
---|---|---|---|
42a0160cde | |||
6bda1ee09d | |||
86effa0452 | |||
2eae48325a | |||
09d5bfef82 | |||
d426df91f0 | |||
5daad52aba | |||
454a460441 | |||
651c3f127d | |||
4ace355374 | |||
968df2b161 | |||
12e7bd2768 | |||
cfbe98d34a | |||
e0f83ca259 | |||
980ec5b855 | |||
a0e6fbbeea | |||
610f9ab142 | |||
fa8c416b77 | |||
1d781bd72c | |||
042b6ddbd6 | |||
c490dcf128 | |||
549c80740d | |||
1a8c44a4ba | |||
bd23d3aa15 | |||
68a523f76a | |||
322500739e | |||
a6a7e56bd1 | |||
7bcf5acb7e | |||
38f17c325a | |||
907711e753 | |||
8cc00fe1a8 | |||
3c58e5201a | |||
795a87fe56 | |||
a6569815af | |||
b95cd06cb1 | |||
356409eb16 | |||
1ba94a526c | |||
0b0de00cc4 | |||
b1fae4bdeb | |||
fc7d428f76 |
200 changed files with 5513 additions and 3712 deletions
4
.editorconfig
Normal file
4
.editorconfig
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[*.{cs,vb}]
|
||||||
|
|
||||||
|
# IDE0046: Convert to conditional expression
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = false
|
|
@ -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.
|
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.
|
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 |
|
| Type | Description |
|
||||||
|:------:| ----------- |
|
|:------:| ----------- |
|
||||||
| `int` | Rank of the user. Used to determine what channels a user can access or what other users the user can moderate. |
|
| `int` | Rank of the user. Used to determine what channels a user can access or what other users the user can moderate. |
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace SharpChat.Misuzu {
|
namespace SharpChat.Misuzu {
|
||||||
public class MisuzuAuthInfo {
|
public class MisuzuAuthInfo {
|
||||||
|
@ -13,18 +12,18 @@ namespace SharpChat.Misuzu {
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("username")]
|
[JsonPropertyName("username")]
|
||||||
public string UserName { get; set; }
|
public string? UserName { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("colour_raw")]
|
[JsonPropertyName("colour_raw")]
|
||||||
public int ColourRaw { get; set; }
|
public int ColourRaw { get; set; }
|
||||||
|
|
||||||
public ChatColour Colour => ChatColour.FromMisuzu(ColourRaw);
|
public Colour Colour => Colour.FromMisuzu(ColourRaw);
|
||||||
|
|
||||||
[JsonPropertyName("hierarchy")]
|
[JsonPropertyName("hierarchy")]
|
||||||
public int Rank { get; set; }
|
public int Rank { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("perms")]
|
[JsonPropertyName("perms")]
|
||||||
public ChatUserPermissions Permissions { get; set; }
|
public UserPermissions Permissions { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("super")]
|
[JsonPropertyName("super")]
|
||||||
public bool IsSuper { get; set; }
|
public bool IsSuper { get; set; }
|
|
@ -7,10 +7,10 @@ namespace SharpChat.Misuzu {
|
||||||
public bool IsBanned { get; set; }
|
public bool IsBanned { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("user_id")]
|
[JsonPropertyName("user_id")]
|
||||||
public string UserId { get; set; }
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("ip_addr")]
|
[JsonPropertyName("ip_addr")]
|
||||||
public string RemoteAddress { get; set; }
|
public string? RemoteAddress { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("is_perma")]
|
[JsonPropertyName("is_perma")]
|
||||||
public bool IsPermanent { get; set; }
|
public bool IsPermanent { get; set; }
|
||||||
|
@ -20,13 +20,13 @@ namespace SharpChat.Misuzu {
|
||||||
|
|
||||||
// only populated in list request
|
// only populated in list request
|
||||||
[JsonPropertyName("user_name")]
|
[JsonPropertyName("user_name")]
|
||||||
public string UserName { get; set; }
|
public string? UserName { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("user_colour")]
|
[JsonPropertyName("user_colour")]
|
||||||
public int UserColourRaw { get; set; }
|
public int UserColourRaw { get; set; }
|
||||||
|
|
||||||
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
||||||
|
|
||||||
public ChatColour UserColour => ChatColour.FromMisuzu(UserColourRaw);
|
public Colour UserColour => Colour.FromMisuzu(UserColourRaw);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -34,9 +34,7 @@ namespace SharpChat.Misuzu {
|
||||||
private CachedValue<string> SecretKey { get; }
|
private CachedValue<string> SecretKey { get; }
|
||||||
|
|
||||||
public MisuzuClient(HttpClient httpClient, IConfig config) {
|
public MisuzuClient(HttpClient httpClient, IConfig config) {
|
||||||
if(config == null)
|
HttpClient = httpClient;
|
||||||
throw new ArgumentNullException(nameof(config));
|
|
||||||
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
||||||
|
|
||||||
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
|
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
|
||||||
SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
|
SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
|
||||||
|
@ -47,11 +45,11 @@ namespace SharpChat.Misuzu {
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CreateBufferSignature(byte[] bytes) {
|
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")));
|
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;
|
method ??= string.Empty;
|
||||||
token ??= string.Empty;
|
token ??= string.Empty;
|
||||||
ipAddr ??= string.Empty;
|
ipAddr ??= string.Empty;
|
||||||
|
@ -77,8 +75,6 @@ namespace SharpChat.Misuzu {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
|
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
|
||||||
if(list == null)
|
|
||||||
throw new ArgumentNullException(nameof(list));
|
|
||||||
if(!list.Any())
|
if(!list.Any())
|
||||||
return;
|
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;
|
userId ??= string.Empty;
|
||||||
ipAddr ??= 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 now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||||
string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
|
string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
|
||||||
string sig = string.Format(BANS_LIST_SIG, 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) {
|
public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
|
||||||
if(banInfo == null)
|
|
||||||
throw new ArgumentNullException(nameof(banInfo));
|
|
||||||
|
|
||||||
string type = kind switch {
|
string type = kind switch {
|
||||||
BanRevokeKind.UserId => "user",
|
BanRevokeKind.UserId => "user",
|
||||||
BanRevokeKind.RemoteAddress => "addr",
|
BanRevokeKind.RemoteAddress => "addr",
|
||||||
|
@ -170,8 +167,8 @@ namespace SharpChat.Misuzu {
|
||||||
};
|
};
|
||||||
|
|
||||||
string target = kind switch {
|
string target = kind switch {
|
||||||
BanRevokeKind.UserId => banInfo.UserId,
|
BanRevokeKind.UserId => banInfo?.UserId ?? string.Empty,
|
||||||
BanRevokeKind.RemoteAddress => banInfo.RemoteAddress,
|
BanRevokeKind.RemoteAddress => banInfo?.RemoteAddress ?? string.Empty,
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -204,9 +201,9 @@ namespace SharpChat.Misuzu {
|
||||||
string reason
|
string reason
|
||||||
) {
|
) {
|
||||||
if(string.IsNullOrWhiteSpace(targetAddr))
|
if(string.IsNullOrWhiteSpace(targetAddr))
|
||||||
throw new ArgumentNullException(nameof(targetAddr));
|
throw new ArgumentException("targetAddr may not be empty", nameof(targetAddr));
|
||||||
if(string.IsNullOrWhiteSpace(modAddr))
|
if(string.IsNullOrWhiteSpace(modAddr))
|
||||||
throw new ArgumentNullException(nameof(modAddr));
|
throw new ArgumentException("modAddr may not be empty", nameof(modAddr));
|
||||||
if(duration <= TimeSpan.Zero)
|
if(duration <= TimeSpan.Zero)
|
||||||
return;
|
return;
|
||||||
|
|
12
SharpChat.Misuzu/SharpChat.Misuzu.csproj
Normal file
12
SharpChat.Misuzu/SharpChat.Misuzu.csproj
Normal 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>
|
36
SharpChat.SockChat/Commands/BanListCommand.cs
Normal file
36
SharpChat.SockChat/Commands/BanListCommand.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
SharpChat.SockChat/Commands/ChannelCreateCommand.cs
Normal file
66
SharpChat.SockChat/Commands/ChannelCreateCommand.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
SharpChat.SockChat/Commands/ChannelDeleteCommand.cs
Normal file
36
SharpChat.SockChat/Commands/ChannelDeleteCommand.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
SharpChat.SockChat/Commands/ChannelJoinCommand.cs
Normal file
48
SharpChat.SockChat/Commands/ChannelJoinCommand.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
SharpChat.SockChat/Commands/ChannelPasswordCommand.cs
Normal file
26
SharpChat.SockChat/Commands/ChannelPasswordCommand.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
SharpChat.SockChat/Commands/ChannelRankCommand.cs
Normal file
28
SharpChat.SockChat/Commands/ChannelRankCommand.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
SharpChat.SockChat/Commands/ISockChatClientCommand.cs
Normal file
6
SharpChat.SockChat/Commands/ISockChatClientCommand.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace SharpChat.SockChat.Commands {
|
||||||
|
public interface ISockChatClientCommand {
|
||||||
|
bool IsMatch(SockChatClientCommandContext ctx);
|
||||||
|
void Dispatch(SockChatClientCommandContext ctx);
|
||||||
|
}
|
||||||
|
}
|
86
SharpChat.SockChat/Commands/KickBanCommand.cs
Normal file
86
SharpChat.SockChat/Commands/KickBanCommand.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
SharpChat.SockChat/Commands/MessageActionCommand.cs
Normal file
22
SharpChat.SockChat/Commands/MessageActionCommand.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
SharpChat.SockChat/Commands/MessageBroadcastCommand.cs
Normal file
24
SharpChat.SockChat/Commands/MessageBroadcastCommand.cs
Normal 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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
SharpChat.SockChat/Commands/MessageDeleteCommand.cs
Normal file
42
SharpChat.SockChat/Commands/MessageDeleteCommand.cs
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
SharpChat.SockChat/Commands/MessageWhisperCommand.cs
Normal file
38
SharpChat.SockChat/Commands/MessageWhisperCommand.cs
Normal 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)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
SharpChat.SockChat/Commands/PardonAddressCommand.cs
Normal file
51
SharpChat.SockChat/Commands/PardonAddressCommand.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
SharpChat.SockChat/Commands/PardonUserCommand.cs
Normal file
59
SharpChat.SockChat/Commands/PardonUserCommand.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
SharpChat.SockChat/Commands/ShutdownRestartCommand.cs
Normal file
40
SharpChat.SockChat/Commands/ShutdownRestartCommand.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
SharpChat.SockChat/Commands/SockChatClientCommandContext.cs
Normal file
34
SharpChat.SockChat/Commands/SockChatClientCommandContext.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
using SharpChat.Packet;
|
using SharpChat.Events;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace SharpChat.Commands {
|
namespace SharpChat.SockChat.Commands {
|
||||||
public class AFKCommand : IChatCommand {
|
public class UserAFKCommand : ISockChatClientCommand {
|
||||||
private const string DEFAULT = "AFK";
|
private const string DEFAULT = "AFK";
|
||||||
private const int MAX_LENGTH = 5;
|
private const int MAX_LENGTH = 5;
|
||||||
|
|
||||||
public bool IsMatch(ChatCommandContext ctx) {
|
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||||
return ctx.NameEquals("afk");
|
return ctx.NameEquals("afk");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispatch(ChatCommandContext ctx) {
|
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||||
string statusText = ctx.Args.FirstOrDefault();
|
string? statusText = ctx.Args.FirstOrDefault();
|
||||||
if(string.IsNullOrWhiteSpace(statusText))
|
if(string.IsNullOrWhiteSpace(statusText))
|
||||||
statusText = DEFAULT;
|
statusText = DEFAULT;
|
||||||
else {
|
else {
|
||||||
|
@ -20,10 +20,10 @@ namespace SharpChat.Commands {
|
||||||
statusText = statusText[..MAX_LENGTH].Trim();
|
statusText = statusText[..MAX_LENGTH].Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Chat.UpdateUser(
|
ctx.Chat.Events.Dispatch(
|
||||||
|
"user:status",
|
||||||
ctx.User,
|
ctx.User,
|
||||||
status: ChatUserStatus.Away,
|
new UserStatusUpdateEventData(UserStatus.Away, statusText)
|
||||||
statusText: statusText
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
61
SharpChat.SockChat/Commands/UserNickCommand.cs
Normal file
61
SharpChat.SockChat/Commands/UserNickCommand.cs
Normal 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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
SharpChat.SockChat/Commands/WhoCommand.cs
Normal file
44
SharpChat.SockChat/Commands/WhoCommand.cs
Normal 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))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
SharpChat.SockChat/Commands/WhoisCommand.cs
Normal file
30
SharpChat.SockChat/Commands/WhoisCommand.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
237
SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs
Normal file
237
SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
SharpChat.SockChat/PacketsC2S/C2SPacketHandlerContext.cs
Normal file
21
SharpChat.SockChat/PacketsC2S/C2SPacketHandlerContext.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
SharpChat.SockChat/PacketsC2S/IC2SPacketHandler.cs
Normal file
6
SharpChat.SockChat/PacketsC2S/IC2SPacketHandler.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace SharpChat.SockChat.PacketsC2S {
|
||||||
|
public interface IC2SPacketHandler {
|
||||||
|
bool IsMatch(C2SPacketHandlerContext ctx);
|
||||||
|
void Handle(C2SPacketHandlerContext ctx);
|
||||||
|
}
|
||||||
|
}
|
60
SharpChat.SockChat/PacketsC2S/PingC2SPacketHandler.cs
Normal file
60
SharpChat.SockChat/PacketsC2S/PingC2SPacketHandler.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs
Normal file
92
SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
SharpChat.SockChat/PacketsS2C/AuthFailS2CPacket.cs
Normal file
38
SharpChat.SockChat/PacketsS2C/AuthFailS2CPacket.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
SharpChat.SockChat/PacketsS2C/AuthSuccessS2CPacket.cs
Normal file
47
SharpChat.SockChat/PacketsS2C/AuthSuccessS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
SharpChat.SockChat/PacketsS2C/BanListResponseS2CPacket.cs
Normal file
32
SharpChat.SockChat/PacketsS2C/BanListResponseS2CPacket.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
SharpChat.SockChat/PacketsS2C/ChannelCreateS2CPacket.cs
Normal file
26
SharpChat.SockChat/PacketsS2C/ChannelCreateS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
SharpChat.SockChat/PacketsS2C/ChannelDeleteS2CPacket.cs
Normal file
16
SharpChat.SockChat/PacketsS2C/ChannelDeleteS2CPacket.cs
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
SharpChat.SockChat/PacketsS2C/ChannelUpdateS2CPacket.cs
Normal file
30
SharpChat.SockChat/PacketsS2C/ChannelUpdateS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
SharpChat.SockChat/PacketsS2C/ChannelsPopulateS2CPacket.cs
Normal file
29
SharpChat.SockChat/PacketsS2C/ChannelsPopulateS2CPacket.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace SharpChat.SockChat.PacketsS2C {
|
||||||
|
public class ClearMessagesAndUsersS2CPacket : ISockChatS2CPacket {
|
||||||
|
public string Pack() {
|
||||||
|
return "8\t3";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
SharpChat.SockChat/PacketsS2C/CommandFormatErrorS2CPacket.cs
Normal file
21
SharpChat.SockChat/PacketsS2C/CommandFormatErrorS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
SharpChat.SockChat/PacketsS2C/FloodWarningS2CPacket.cs
Normal file
21
SharpChat.SockChat/PacketsS2C/FloodWarningS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
SharpChat.SockChat/PacketsS2C/ForceDisconnectS2CPacket.cs
Normal file
19
SharpChat.SockChat/PacketsS2C/ForceDisconnectS2CPacket.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
SharpChat.SockChat/PacketsS2C/ISockChatS2CPacket.cs
Normal file
5
SharpChat.SockChat/PacketsS2C/ISockChatS2CPacket.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace SharpChat.SockChat.PacketsS2C {
|
||||||
|
public interface ISockChatS2CPacket {
|
||||||
|
string Pack();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
SharpChat.SockChat/PacketsS2C/MOTDS2CPacket.cs
Normal file
21
SharpChat.SockChat/PacketsS2C/MOTDS2CPacket.cs
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
SharpChat.SockChat/PacketsS2C/MessageAddLogS2CPacket.cs
Normal file
83
SharpChat.SockChat/PacketsS2C/MessageAddLogS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
SharpChat.SockChat/PacketsS2C/MessageAddS2CPacket.cs
Normal file
47
SharpChat.SockChat/PacketsS2C/MessageAddS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
SharpChat.SockChat/PacketsS2C/MessageBroadcastS2CPacket.cs
Normal file
24
SharpChat.SockChat/PacketsS2C/MessageBroadcastS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs
Normal file
13
SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
SharpChat.SockChat/PacketsS2C/PardonResponseS2CPacket.cs
Normal file
24
SharpChat.SockChat/PacketsS2C/PardonResponseS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
SharpChat.SockChat/PacketsS2C/PongS2CPacket.cs
Normal file
7
SharpChat.SockChat/PacketsS2C/PongS2CPacket.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace SharpChat.SockChat.PacketsS2C {
|
||||||
|
public class PongS2CPacket : ISockChatS2CPacket {
|
||||||
|
public string Pack() {
|
||||||
|
return "0\tpong";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
SharpChat.SockChat/PacketsS2C/UserChannelJoinLogS2CPacket.cs
Normal file
28
SharpChat.SockChat/PacketsS2C/UserChannelJoinLogS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
SharpChat.SockChat/PacketsS2C/UserChannelJoinS2CPacket.cs
Normal file
42
SharpChat.SockChat/PacketsS2C/UserChannelJoinS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
SharpChat.SockChat/PacketsS2C/UserChannelLeaveS2CPacket.cs
Normal file
19
SharpChat.SockChat/PacketsS2C/UserChannelLeaveS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
SharpChat.SockChat/PacketsS2C/UserConnectLogS2CPacket.cs
Normal file
28
SharpChat.SockChat/PacketsS2C/UserConnectLogS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
SharpChat.SockChat/PacketsS2C/UserConnectS2CPacket.cs
Normal file
49
SharpChat.SockChat/PacketsS2C/UserConnectS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
SharpChat.SockChat/PacketsS2C/UserDisconnectLogS2CPacket.cs
Normal file
38
SharpChat.SockChat/PacketsS2C/UserDisconnectLogS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
SharpChat.SockChat/PacketsS2C/UserDisconnectS2CPacket.cs
Normal file
42
SharpChat.SockChat/PacketsS2C/UserDisconnectS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
SharpChat.SockChat/PacketsS2C/UserNameInUseErrorS2CPacket.cs
Normal file
24
SharpChat.SockChat/PacketsS2C/UserNameInUseErrorS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
SharpChat.SockChat/PacketsS2C/UserNickChangeLogS2CPacket.cs
Normal file
32
SharpChat.SockChat/PacketsS2C/UserNickChangeLogS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
SharpChat.SockChat/PacketsS2C/UserNickChangeS2CPacket.cs
Normal file
32
SharpChat.SockChat/PacketsS2C/UserNickChangeS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
SharpChat.SockChat/PacketsS2C/UserNotFoundErrorS2CPacket.cs
Normal file
24
SharpChat.SockChat/PacketsS2C/UserNotFoundErrorS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
SharpChat.SockChat/PacketsS2C/UserUpdateS2CPacket.cs
Normal file
39
SharpChat.SockChat/PacketsS2C/UserUpdateS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
SharpChat.SockChat/PacketsS2C/UsersPopulateS2CPacket.cs
Normal file
37
SharpChat.SockChat/PacketsS2C/UsersPopulateS2CPacket.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
SharpChat.SockChat/PacketsS2C/WhoChannelResponseS2CPacket.cs
Normal file
46
SharpChat.SockChat/PacketsS2C/WhoChannelResponseS2CPacket.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
SharpChat.SockChat/PacketsS2C/WhoServerResponseS2CPacket.cs
Normal file
40
SharpChat.SockChat/PacketsS2C/WhoServerResponseS2CPacket.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
SharpChat.SockChat/PacketsS2C/WhoisResponseS2CPacket.cs
Normal file
27
SharpChat.SockChat/PacketsS2C/WhoisResponseS2CPacket.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
SharpChat.SockChat/SharpChat.SockChat.csproj
Normal file
17
SharpChat.SockChat/SharpChat.SockChat.csproj
Normal 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>
|
43
SharpChat.SockChat/SockChatConnectionInfo.cs
Normal file
43
SharpChat.SockChat/SockChatConnectionInfo.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
565
SharpChat.SockChat/SockChatContext.cs
Normal file
565
SharpChat.SockChat/SockChatContext.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
265
SharpChat.SockChat/SockChatServer.cs
Normal file
265
SharpChat.SockChat/SockChatServer.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
SharpChat.SockChat/SockChatUtility.cs
Normal file
75
SharpChat.SockChat/SockChatUtility.cs
Normal 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("<", "<").Replace(">", ">").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(
|
||||||
|
"<{0}>_{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(
|
||||||
|
"<{0}>_{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("<")) {
|
||||||
|
int gt = name.IndexOf(">_");
|
||||||
|
if(gt > 0) {
|
||||||
|
gt += 5;
|
||||||
|
name = name[gt..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(name.StartsWith("~")) {
|
||||||
|
target = UsersContext.NameTarget.NickName;
|
||||||
|
name = name[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (name, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
using Fleck;
|
#nullable disable
|
||||||
|
|
||||||
|
using Fleck;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -14,13 +16,13 @@ using System.Text;
|
||||||
// https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs
|
// https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat {
|
||||||
public class SharpChatWebSocketServer : IWebSocketServer {
|
public class SockChatWebSocketServer : IWebSocketServer {
|
||||||
|
|
||||||
private readonly string _scheme;
|
private readonly string _scheme;
|
||||||
private readonly IPAddress _locationIP;
|
private readonly IPAddress _locationIP;
|
||||||
private Action<IWebSocketConnection> _config;
|
private Action<IWebSocketConnection> _config;
|
||||||
|
|
||||||
public SharpChatWebSocketServer(string location, bool supportDualStack = true) {
|
public SockChatWebSocketServer(string location, bool supportDualStack = true) {
|
||||||
Uri uri = new(location);
|
Uri uri = new(location);
|
||||||
|
|
||||||
Port = uri.Port;
|
Port = uri.Port;
|
||||||
|
@ -115,7 +117,8 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClientConnect(ISocket clientSocket) {
|
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()));
|
FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString()));
|
||||||
ListenForClients();
|
ListenForClients();
|
|
@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\Shar
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DF7A7073-A67A-4D93-92C6-F9D0F95E2359}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DF7A7073-A67A-4D93-92C6-F9D0F95E2359}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
.editorconfig = .editorconfig
|
||||||
.gitattributes = .gitattributes
|
.gitattributes = .gitattributes
|
||||||
.gitignore = .gitignore
|
.gitignore = .gitignore
|
||||||
LICENSE = LICENSE
|
LICENSE = LICENSE
|
||||||
|
@ -15,6 +16,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
start.sh = start.sh
|
start.sh = start.sh
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -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 == '_';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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("<{0}>_", 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}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
Loading…
Add table
Reference in a new issue