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.
|
||||
Which of the methods is used remains consistent per server however, so the result of a test can be cached.
|
||||
|
||||
Note that this string MAY be empty if sent by the bot user (`-1`).
|
||||
|
||||
| Type | Description |
|
||||
|:------:| ----------- |
|
||||
| `int` | Rank of the user. Used to determine what channels a user can access or what other users the user can moderate. |
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharpChat.Misuzu {
|
||||
public class MisuzuAuthInfo {
|
||||
|
@ -13,18 +12,18 @@ namespace SharpChat.Misuzu {
|
|||
public long UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string UserName { get; set; }
|
||||
public string? UserName { get; set; }
|
||||
|
||||
[JsonPropertyName("colour_raw")]
|
||||
public int ColourRaw { get; set; }
|
||||
|
||||
public ChatColour Colour => ChatColour.FromMisuzu(ColourRaw);
|
||||
public Colour Colour => Colour.FromMisuzu(ColourRaw);
|
||||
|
||||
[JsonPropertyName("hierarchy")]
|
||||
public int Rank { get; set; }
|
||||
|
||||
[JsonPropertyName("perms")]
|
||||
public ChatUserPermissions Permissions { get; set; }
|
||||
public UserPermissions Permissions { get; set; }
|
||||
|
||||
[JsonPropertyName("super")]
|
||||
public bool IsSuper { get; set; }
|
|
@ -7,10 +7,10 @@ namespace SharpChat.Misuzu {
|
|||
public bool IsBanned { get; set; }
|
||||
|
||||
[JsonPropertyName("user_id")]
|
||||
public string UserId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("ip_addr")]
|
||||
public string RemoteAddress { get; set; }
|
||||
public string? RemoteAddress { get; set; }
|
||||
|
||||
[JsonPropertyName("is_perma")]
|
||||
public bool IsPermanent { get; set; }
|
||||
|
@ -20,13 +20,13 @@ namespace SharpChat.Misuzu {
|
|||
|
||||
// only populated in list request
|
||||
[JsonPropertyName("user_name")]
|
||||
public string UserName { get; set; }
|
||||
public string? UserName { get; set; }
|
||||
|
||||
[JsonPropertyName("user_colour")]
|
||||
public int UserColourRaw { get; set; }
|
||||
|
||||
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
||||
|
||||
public ChatColour UserColour => ChatColour.FromMisuzu(UserColourRaw);
|
||||
public Colour UserColour => Colour.FromMisuzu(UserColourRaw);
|
||||
}
|
||||
}
|
|
@ -34,9 +34,7 @@ namespace SharpChat.Misuzu {
|
|||
private CachedValue<string> SecretKey { get; }
|
||||
|
||||
public MisuzuClient(HttpClient httpClient, IConfig config) {
|
||||
if(config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
HttpClient = httpClient;
|
||||
|
||||
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
|
||||
SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
|
||||
|
@ -47,11 +45,11 @@ namespace SharpChat.Misuzu {
|
|||
}
|
||||
|
||||
public string CreateBufferSignature(byte[] bytes) {
|
||||
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey));
|
||||
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey.Value ?? string.Empty));
|
||||
return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
|
||||
}
|
||||
|
||||
public async Task<MisuzuAuthInfo> AuthVerifyAsync(string method, string token, string ipAddr) {
|
||||
public async Task<MisuzuAuthInfo?> AuthVerifyAsync(string method, string token, string ipAddr) {
|
||||
method ??= string.Empty;
|
||||
token ??= string.Empty;
|
||||
ipAddr ??= string.Empty;
|
||||
|
@ -77,8 +75,6 @@ namespace SharpChat.Misuzu {
|
|||
}
|
||||
|
||||
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
|
||||
if(list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
if(!list.Any())
|
||||
return;
|
||||
|
||||
|
@ -114,7 +110,11 @@ namespace SharpChat.Misuzu {
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<MisuzuBanInfo> CheckBanAsync(string userId = null, string ipAddr = null, bool userIdIsName = false) {
|
||||
public async Task<MisuzuBanInfo?> CheckBanAsync(
|
||||
string? userId = null,
|
||||
string? ipAddr = null,
|
||||
bool userIdIsName = false
|
||||
) {
|
||||
userId ??= string.Empty;
|
||||
ipAddr ??= string.Empty;
|
||||
|
||||
|
@ -136,7 +136,7 @@ namespace SharpChat.Misuzu {
|
|||
);
|
||||
}
|
||||
|
||||
public async Task<MisuzuBanInfo[]> GetBanListAsync() {
|
||||
public async Task<MisuzuBanInfo[]?> GetBanListAsync() {
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
|
||||
string sig = string.Format(BANS_LIST_SIG, now);
|
||||
|
@ -160,9 +160,6 @@ namespace SharpChat.Misuzu {
|
|||
}
|
||||
|
||||
public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
|
||||
if(banInfo == null)
|
||||
throw new ArgumentNullException(nameof(banInfo));
|
||||
|
||||
string type = kind switch {
|
||||
BanRevokeKind.UserId => "user",
|
||||
BanRevokeKind.RemoteAddress => "addr",
|
||||
|
@ -170,8 +167,8 @@ namespace SharpChat.Misuzu {
|
|||
};
|
||||
|
||||
string target = kind switch {
|
||||
BanRevokeKind.UserId => banInfo.UserId,
|
||||
BanRevokeKind.RemoteAddress => banInfo.RemoteAddress,
|
||||
BanRevokeKind.UserId => banInfo?.UserId ?? string.Empty,
|
||||
BanRevokeKind.RemoteAddress => banInfo?.RemoteAddress ?? string.Empty,
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
|
@ -204,9 +201,9 @@ namespace SharpChat.Misuzu {
|
|||
string reason
|
||||
) {
|
||||
if(string.IsNullOrWhiteSpace(targetAddr))
|
||||
throw new ArgumentNullException(nameof(targetAddr));
|
||||
throw new ArgumentException("targetAddr may not be empty", nameof(targetAddr));
|
||||
if(string.IsNullOrWhiteSpace(modAddr))
|
||||
throw new ArgumentNullException(nameof(modAddr));
|
||||
throw new ArgumentException("modAddr may not be empty", nameof(modAddr));
|
||||
if(duration <= TimeSpan.Zero)
|
||||
return;
|
||||
|
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;
|
||||
|
||||
namespace SharpChat.Commands {
|
||||
public class AFKCommand : IChatCommand {
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class UserAFKCommand : ISockChatClientCommand {
|
||||
private const string DEFAULT = "AFK";
|
||||
private const int MAX_LENGTH = 5;
|
||||
|
||||
public bool IsMatch(ChatCommandContext ctx) {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("afk");
|
||||
}
|
||||
|
||||
public void Dispatch(ChatCommandContext ctx) {
|
||||
string statusText = ctx.Args.FirstOrDefault();
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
string? statusText = ctx.Args.FirstOrDefault();
|
||||
if(string.IsNullOrWhiteSpace(statusText))
|
||||
statusText = DEFAULT;
|
||||
else {
|
||||
|
@ -20,10 +20,10 @@ namespace SharpChat.Commands {
|
|||
statusText = statusText[..MAX_LENGTH].Trim();
|
||||
}
|
||||
|
||||
ctx.Chat.UpdateUser(
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"user:status",
|
||||
ctx.User,
|
||||
status: ChatUserStatus.Away,
|
||||
statusText: statusText
|
||||
new UserStatusUpdateEventData(UserStatus.Away, statusText)
|
||||
);
|
||||
}
|
||||
}
|
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.Collections.Generic;
|
||||
using System.IO;
|
||||
|
@ -14,13 +16,13 @@ using System.Text;
|
|||
// https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs
|
||||
|
||||
namespace SharpChat {
|
||||
public class SharpChatWebSocketServer : IWebSocketServer {
|
||||
public class SockChatWebSocketServer : IWebSocketServer {
|
||||
|
||||
private readonly string _scheme;
|
||||
private readonly IPAddress _locationIP;
|
||||
private Action<IWebSocketConnection> _config;
|
||||
|
||||
public SharpChatWebSocketServer(string location, bool supportDualStack = true) {
|
||||
public SockChatWebSocketServer(string location, bool supportDualStack = true) {
|
||||
Uri uri = new(location);
|
||||
|
||||
Port = uri.Port;
|
||||
|
@ -115,7 +117,8 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
private void OnClientConnect(ISocket clientSocket) {
|
||||
if(clientSocket == null) return; // socket closed
|
||||
if(clientSocket == null)
|
||||
return; // socket closed
|
||||
|
||||
FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString()));
|
||||
ListenForClients();
|
|
@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\Shar
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DF7A7073-A67A-4D93-92C6-F9D0F95E2359}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitattributes = .gitattributes
|
||||
.gitignore = .gitignore
|
||||
LICENSE = LICENSE
|
||||
|
@ -15,6 +16,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
start.sh = start.sh
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChatCommon", "SharpChatCommon\SharpChatCommon.csproj", "{B2228E3C-E0DB-4AAF-A603-2A822B531F76}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.SockChat", "SharpChat.SockChat\SharpChat.SockChat.csproj", "{4D48CCFB-5D3B-4AB6-AF94-04377474078C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.Misuzu", "SharpChat.Misuzu\SharpChat.Misuzu.csproj", "{08FD8B99-011A-43F9-A6C9-A3C1979604CF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -25,6 +32,18 @@ Global
|
|||
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2228E3C-E0DB-4AAF-A603-2A822B531F76}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4D48CCFB-5D3B-4AB6-AF94-04377474078C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08FD8B99-011A-43F9-A6C9-A3C1979604CF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
@ -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