Updated to .NET 9.0

This commit is contained in:
flash 2025-04-25 15:49:46 +00:00
parent b026bad176
commit 1c23ffbbe8
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
61 changed files with 344 additions and 753 deletions

View file

@ -3,38 +3,22 @@ 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 class ChatChannel(
string name,
string password = "",
bool isTemporary = false,
int rank = 0,
long ownerId = 0
) {
public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
public string Password { get; set; } = password ?? string.Empty;
public bool IsTemporary { get; set; } = isTemporary;
public int Rank { get; set; } = rank;
public long OwnerId { get; set; } = ownerId;
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();

View file

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
namespace SharpChat {
public struct ChatColour {
public readonly struct ChatColour {
public byte Red { get; }
public byte Green { get; }
public byte Blue { get; }

View file

@ -17,8 +17,7 @@ namespace SharpChat {
ChatConnection connection,
ChatChannel channel
) {
if(text == null)
throw new ArgumentNullException(nameof(text));
ArgumentNullException.ThrowIfNull(text);
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
User = user ?? throw new ArgumentNullException(nameof(user));
@ -27,7 +26,7 @@ namespace SharpChat {
string[] parts = text[1..].Split(' ');
Name = parts.First().Replace(".", string.Empty);
Args = parts.Skip(1).ToArray();
Args = [.. parts.Skip(1)];
}
public ChatCommandContext(

View file

@ -37,8 +37,8 @@ namespace SharpChat {
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))
&& sock.ConnectionInfo.Headers.TryGetValue("X-Real-IP", out string addrStr)
&& IPAddress.TryParse(addrStr, out IPAddress realAddr))
addr = realAddr;
RemoteAddress = addr;

View file

@ -8,22 +8,18 @@ using System.Net;
using System.Threading;
namespace SharpChat {
public class ChatContext {
public class ChatContext(IEventStorage evtStore) {
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 HashSet<ChatChannel> Channels { get; } = [];
public HashSet<ChatConnection> Connections { get; } = [];
public HashSet<ChatUser> Users { get; } = [];
public IEventStorage Events { get; } = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = [];
public Dictionary<long, ChatChannel> UserLastChannel { get; } = [];
public void DispatchEvent(IChatEvent eventInfo) {
if(eventInfo is MessageCreateEvent mce) {
@ -34,7 +30,7 @@ namespace SharpChat {
// e.g. nook sees @Arysil and Arysil sees @nook
// this entire routine is garbage, channels should probably in the db
if(!mce.ChannelName.StartsWith("@"))
if(!mce.ChannelName.StartsWith('@'))
return;
IEnumerable<long> uids = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1);
@ -110,21 +106,21 @@ namespace SharpChat {
}
public string[] GetUserChannelNames(ChatUser user) {
return ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName).ToArray();
return [.. ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName)];
}
public ChatChannel[] GetUserChannels(ChatUser user) {
string[] names = GetUserChannelNames(user);
return Channels.Where(c => names.Any(n => c.NameEquals(n))).ToArray();
return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
}
public long[] GetChannelUserIds(ChatChannel channel) {
return ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId).ToArray();
return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)];
}
public ChatUser[] GetChannelUsers(ChatChannel channel) {
long[] ids = GetChannelUserIds(channel);
return Users.Where(u => ids.Contains(u.UserId)).ToArray();
return [.. Users.Where(u => ids.Contains(u.UserId))];
}
public void UpdateUser(
@ -139,11 +135,10 @@ namespace SharpChat {
bool? isSuper = null,
bool silent = false
) {
if(user == null)
throw new ArgumentNullException(nameof(user));
ArgumentNullException.ThrowIfNull(user);
bool hasChanged = false;
string previousName = null;
string previousName = string.Empty;
if(userName != null && !user.UserName.Equals(userName)) {
user.UserName = userName;
@ -210,11 +205,11 @@ namespace SharpChat {
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);
Events.AddEvent(SharpId.Next(), "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
}
conn.Send(new AuthSuccessPacket(user, chan, conn, maxMsgLength));
conn.Send(new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank)));
conn.Send(new AuthSuccessPacket(user, chan, maxMsgLength));
conn.Send(new ContextUsersPacket(GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)));
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
conn.Send(new ContextMessagePacket(msg));
@ -238,7 +233,7 @@ namespace SharpChat {
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);
Events.AddEvent(SharpId.Next(), "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, StoredEventFlags.Log);
if(chan.IsTemporary && chan.IsOwner(user))
RemoveChannel(chan);
@ -275,12 +270,12 @@ namespace SharpChat {
ChatChannel oldChan = UserLastChannel[user.UserId];
SendTo(oldChan, new UserChannelLeavePacket(user));
Events.AddEvent("chan:leave", user, oldChan, flags: StoredEventFlags.Log);
Events.AddEvent(SharpId.Next(), "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
SendTo(chan, new UserChannelJoinPacket(user));
Events.AddEvent("chan:join", user, oldChan, flags: StoredEventFlags.Log);
Events.AddEvent(SharpId.Next(), "chan:join", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
SendTo(user, new ContextClearPacket(chan, ContextClearMode.MessagesUsers));
SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank)));
SendTo(user, new ContextClearPacket(ContextClearMode.MessagesUsers));
SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)));
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
SendTo(user, new ContextMessagePacket(msg));
@ -296,8 +291,7 @@ namespace SharpChat {
}
public void Send(IServerPacket packet) {
if(packet == null)
throw new ArgumentNullException(nameof(packet));
ArgumentNullException.ThrowIfNull(packet);
foreach(ChatConnection conn in Connections)
if(conn.IsAuthed)
@ -305,10 +299,8 @@ namespace SharpChat {
}
public void SendTo(ChatUser user, IServerPacket packet) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(packet);
foreach(ChatConnection conn in Connections)
if(conn.IsAlive && conn.User == user)
@ -316,10 +308,8 @@ namespace SharpChat {
}
public void SendTo(ChatChannel channel, IServerPacket packet) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
ArgumentNullException.ThrowIfNull(channel);
ArgumentNullException.ThrowIfNull(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));
@ -328,10 +318,8 @@ namespace SharpChat {
}
public void SendToUserChannels(ChatUser user, IServerPacket packet) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(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))));
@ -340,12 +328,11 @@ namespace SharpChat {
}
public IPAddress[] GetRemoteAddresses(ChatUser user) {
return Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct().ToArray();
return [.. Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct()];
}
public void ForceChannel(ChatUser user, ChatChannel chan = null) {
if(user == null)
throw new ArgumentNullException(nameof(user));
ArgumentNullException.ThrowIfNull(user);
if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
throw new ArgumentException("no channel???");
@ -354,8 +341,7 @@ namespace SharpChat {
}
public void UpdateChannel(ChatChannel channel, bool? temporary = null, int? hierarchy = null, string password = null) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
ArgumentNullException.ThrowIfNull(channel);
if(!Channels.Contains(channel))
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
@ -375,7 +361,7 @@ namespace SharpChat {
}
public void RemoveChannel(ChatChannel channel) {
if(channel == null || !Channels.Any())
if(channel == null || Channels.Count < 1)
return;
ChatChannel defaultChannel = Channels.FirstOrDefault();

View file

@ -1,20 +1,14 @@
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 class ChatPacketHandlerContext(
string text,
ChatContext chat,
ChatConnection connection
) {
public string Text { get; } = text ?? throw new ArgumentNullException(nameof(text));
public ChatContext Chat { get; } = chat ?? throw new ArgumentNullException(nameof(chat));
public ChatConnection Connection { get; } = connection ?? throw new ArgumentNullException(nameof(connection));
public bool CheckPacketId(string packetId) {
return Text == packetId || Text.StartsWith(packetId + '\t');

View file

@ -4,20 +4,29 @@ using System.Text;
using SharpChat.Commands;
namespace SharpChat {
public class ChatUser : IEquatable<ChatUser> {
public class ChatUser(
long userId,
string userName,
ChatColour colour,
int rank,
ChatUserPermissions perms,
string nickName = "",
ChatUserStatus status = ChatUserStatus.Online,
string statusText = ""
) {
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 long UserId { get; } = userId;
public string UserName { get; set; } = userName ?? throw new ArgumentNullException(nameof(userName));
public ChatColour Colour { get; set; } = colour;
public int Rank { get; set; } = rank;
public ChatUserPermissions Permissions { get; set; } = perms;
public bool IsSuper { get; set; }
public string NickName { get; set; }
public ChatUserStatus Status { get; set; }
public string StatusText { get; set; }
public string NickName { get; set; } = nickName ?? string.Empty;
public ChatUserStatus Status { get; set; } = status;
public string StatusText { get; set; } = statusText ?? string.Empty;
public string LegacyName => string.IsNullOrWhiteSpace(NickName) ? UserName : $"~{NickName}";
@ -27,7 +36,7 @@ namespace SharpChat {
if(Status == ChatUserStatus.Away) {
string statusText = StatusText.Trim();
StringInfo sti = new StringInfo(statusText);
StringInfo sti = new(statusText);
if(Encoding.UTF8.GetByteCount(statusText) > AFKCommand.MAX_BYTES
|| sti.LengthInTextElements > AFKCommand.MAX_GRAPHEMES)
statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, AFKCommand.MAX_GRAPHEMES)).Trim();
@ -41,27 +50,6 @@ namespace SharpChat {
}
}
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;
@ -102,14 +90,6 @@ namespace SharpChat {
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}"

View file

@ -21,7 +21,7 @@ namespace SharpChat.Commands {
else {
statusText = statusText.Trim();
StringInfo sti = new StringInfo(statusText);
StringInfo sti = new(statusText);
if(Encoding.UTF8.GetByteCount(statusText) > MAX_BYTES
|| sti.LengthInTextElements > MAX_GRAPHEMES)
statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, MAX_GRAPHEMES)).Trim();

View file

@ -10,7 +10,7 @@ namespace SharpChat.Commands {
}
public void Dispatch(ChatCommandContext ctx) {
if(!ctx.Args.Any())
if(ctx.Args.Length < 1)
return;
string actionStr = string.Join(' ', ctx.Args);
@ -19,8 +19,13 @@ namespace SharpChat.Commands {
ctx.Chat.DispatchEvent(new MessageCreateEvent(
SharpId.Next(),
ctx.Channel,
ctx.User,
ctx.Channel.Name,
ctx.User.UserId,
ctx.User.UserName,
ctx.User.Colour,
ctx.User.Rank,
ctx.User.NickName,
ctx.User.Permissions,
DateTimeOffset.Now,
actionStr,
false, true, false

View file

@ -4,12 +4,8 @@ using System;
using System.Threading.Tasks;
namespace SharpChat.Commands {
public class BanListCommand : IChatCommand {
private readonly MisuzuClient Misuzu;
public BanListCommand(MisuzuClient msz) {
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
}
public class BanListCommand(MisuzuClient msz) : IChatCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public bool IsMatch(ChatCommandContext ctx) {
return ctx.NameEquals("bans")

View file

@ -18,7 +18,12 @@ namespace SharpChat.Commands {
ctx.Chat.DispatchEvent(new MessageCreateEvent(
SharpId.Next(),
string.Empty,
ctx.User,
ctx.User.UserId,
ctx.User.UserName,
ctx.User.Colour,
ctx.User.Rank,
ctx.User.NickName,
ctx.User.Permissions,
DateTimeOffset.Now,
string.Join(' ', ctx.Args),
false, false, true

View file

@ -16,7 +16,7 @@ namespace SharpChat.Commands {
string firstArg = ctx.Args.First();
bool createChanHasHierarchy;
if(!ctx.Args.Any() || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) {
if(ctx.Args.Length < 1 || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR));
return;
}
@ -44,9 +44,10 @@ namespace SharpChat.Commands {
}
ChatChannel createChan = new(
ctx.User, createChanName,
createChanName,
isTemporary: !ctx.User.Can(ChatUserPermissions.SetChannelPermanent),
rank: createChanHierarchy
rank: createChanHierarchy,
ownerId: ctx.User.UserId
);
ctx.Chat.Channels.Add(createChan);

View file

@ -11,7 +11,7 @@ namespace SharpChat.Commands {
}
public void Dispatch(ChatCommandContext ctx) {
if(!ctx.Args.Any() || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) {
if(ctx.Args.Length < 1 || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR));
return;
}

View file

@ -5,12 +5,8 @@ using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.Commands {
public class KickBanCommand : IChatCommand {
private readonly MisuzuClient Misuzu;
public KickBanCommand(MisuzuClient msz) {
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
}
public class KickBanCommand(MisuzuClient msz) : IChatCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public bool IsMatch(ChatCommandContext ctx) {
return ctx.NameEquals("kick")

View file

@ -45,7 +45,7 @@ namespace SharpChat.Commands {
else if(string.IsNullOrEmpty(nickStr))
nickStr = string.Empty;
else {
StringInfo nsi = new StringInfo(nickStr);
StringInfo nsi = new(nickStr);
if(Encoding.UTF8.GetByteCount(nickStr) > MAX_BYTES
|| nsi.LengthInTextElements > MAX_GRAPHEMES)
nickStr = nsi.SubstringByTextElements(0, Math.Min(nsi.LengthInTextElements, MAX_GRAPHEMES)).Trim();

View file

@ -6,12 +6,8 @@ using System.Net;
using System.Threading.Tasks;
namespace SharpChat.Commands {
public class PardonAddressCommand : IChatCommand {
private readonly MisuzuClient Misuzu;
public PardonAddressCommand(MisuzuClient msz) {
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
}
public class PardonAddressCommand(MisuzuClient msz) : IChatCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public bool IsMatch(ChatCommandContext ctx) {
return ctx.NameEquals("pardonip")

View file

@ -5,12 +5,8 @@ using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.Commands {
public class PardonUserCommand : IChatCommand {
private readonly MisuzuClient Misuzu;
public PardonUserCommand(MisuzuClient msz) {
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
}
public class PardonUserCommand(MisuzuClient msz) : IChatCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public bool IsMatch(ChatCommandContext ctx) {
return ctx.NameEquals("pardon")

View file

@ -15,7 +15,7 @@ namespace SharpChat.Commands {
return;
}
if(!ctx.Args.Any() || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) {
if(ctx.Args.Length < 1 || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.INSUFFICIENT_HIERARCHY));
return;
}

View file

@ -3,14 +3,9 @@ using System;
using System.Threading;
namespace SharpChat.Commands {
public class ShutdownRestartCommand : IChatCommand {
private readonly ManualResetEvent WaitHandle;
private readonly Func<bool> ShutdownCheck;
public ShutdownRestartCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) {
WaitHandle = waitHandle ?? throw new ArgumentNullException(nameof(waitHandle));
ShutdownCheck = shutdownCheck ?? throw new ArgumentNullException(nameof(shutdownCheck));
}
public class ShutdownRestartCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : IChatCommand {
private readonly ManualResetEvent WaitHandle = waitHandle ?? throw new ArgumentNullException(nameof(waitHandle));
private readonly Func<bool> ShutdownCheck = shutdownCheck ?? throw new ArgumentNullException(nameof(shutdownCheck));
public bool IsMatch(ChatCommandContext ctx) {
return ctx.NameEquals("shutdown")

View file

@ -30,7 +30,12 @@ namespace SharpChat.Commands {
ctx.Chat.DispatchEvent(new MessageCreateEvent(
SharpId.Next(),
ChatUser.GetDMChannelName(ctx.User, whisperUser),
ctx.User,
ctx.User.UserId,
ctx.User.UserName,
ctx.User.Colour,
ctx.User.Rank,
ctx.User.NickName,
ctx.User.Permissions,
DateTimeOffset.Now,
string.Join(' ', ctx.Args.Skip(1)),
true, false, false

View file

@ -1,11 +1,9 @@
using System;
namespace SharpChat.Config {
public class CachedValue<T> {
private IConfig Config { get; }
private string Name { get; }
private TimeSpan Lifetime { get; }
private T Fallback { get; }
public class CachedValue<T>(IConfig config, string name, TimeSpan lifetime, T fallback) {
private IConfig Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
private string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
private object ConfigAccess { get; } = new();
private object CurrentValue { get; set; }
@ -15,9 +13,9 @@ namespace SharpChat.Config {
get {
lock(ConfigAccess) { // this lock doesn't really make sense since it doesn't affect other config calls
DateTimeOffset now = DateTimeOffset.Now;
if((now - LastRead) >= Lifetime) {
if((now - LastRead) >= lifetime) {
LastRead = now;
CurrentValue = Config.ReadValue(Name, Fallback);
CurrentValue = Config.ReadValue(Name, fallback);
Logger.Debug($"Read {Name} ({CurrentValue})");
}
}
@ -27,15 +25,6 @@ namespace SharpChat.Config {
public static implicit operator T(CachedValue<T> val) => val.Value;
public CachedValue(IConfig config, string name, TimeSpan lifetime, T fallback) {
Config = config ?? throw new ArgumentNullException(nameof(config));
Name = name ?? throw new ArgumentNullException(nameof(name));
Lifetime = lifetime;
Fallback = fallback;
if(string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name cannot be empty.", nameof(name));
}
public void Refresh() {
LastRead = DateTimeOffset.MinValue;
}

View file

@ -6,11 +6,6 @@ namespace SharpChat.Config {
public ConfigException(string message, Exception ex) : base(message, ex) { }
}
public class ConfigLockException : ConfigException {
public ConfigLockException() : base("Unable to acquire lock for reading configuration.") { }
}
public class ConfigTypeException : ConfigException {
public ConfigTypeException(Exception ex) : base("Given type does not match the value in the configuration.", ex) { }
}
public class ConfigLockException() : ConfigException("Unable to acquire lock for reading configuration.") {}
public class ConfigTypeException(Exception ex) : ConfigException("Given type does not match the value in the configuration.", ex) {}
}

View file

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharpChat.Config {
public interface IConfig : IDisposable {

View file

@ -1,18 +1,9 @@
using System;
namespace SharpChat.Config {
public class ScopedConfig : IConfig {
private IConfig Config { get; }
private string Prefix { get; }
public ScopedConfig(IConfig config, string prefix) {
Config = config ?? throw new ArgumentNullException(nameof(config));
Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix));
if(string.IsNullOrWhiteSpace(prefix))
throw new ArgumentException("Prefix must exist.", nameof(prefix));
if(Prefix[^1] != ':')
Prefix += ':';
}
public class ScopedConfig(IConfig config, string prefix) : IConfig {
private IConfig Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
private string Prefix { get; } = prefix ?? throw new ArgumentNullException(nameof(prefix));
private string GetName(string name) {
return Prefix + name;

View file

@ -13,9 +13,6 @@ namespace SharpChat.Config {
private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15);
public StreamConfig(string fileName)
: this(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) { }
public StreamConfig(Stream stream) {
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
if(!Stream.CanRead)
@ -26,6 +23,10 @@ namespace SharpChat.Config {
Lock = new Mutex();
}
public static StreamConfig FromPath(string fileName) {
return new StreamConfig(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite));
}
public string ReadValue(string name, string fallback = null) {
if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong
throw new ConfigLockException();
@ -39,7 +40,7 @@ namespace SharpChat.Config {
continue;
line = line.TrimStart();
if(line.StartsWith(";") || line.StartsWith("#"))
if(line.StartsWith(';') || line.StartsWith('#'))
continue;
string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
@ -85,6 +86,11 @@ namespace SharpChat.Config {
}
public IConfig ScopeTo(string prefix) {
if(string.IsNullOrWhiteSpace(prefix))
throw new ArgumentException("Prefix must exist.", nameof(prefix));
if(prefix[^1] != ':')
prefix += ':';
return new ScopedConfig(this, prefix);
}

View file

@ -4,31 +4,18 @@ namespace SharpChat.EventStorage
{
public interface IEventStorage {
void AddEvent(
long id, string type,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
);
void AddEvent(
long id, string type,
long id,
string type,
string channelName,
long senderId,
string senderName,
ChatColour senderColour,
int senderRank,
string senderNick,
ChatUserPermissions senderPerms,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
);
void AddEvent(
long id, string type,
long senderId, string senderName, ChatColour senderColour, int senderRank, string senderNick, ChatUserPermissions senderPerms,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
);
void AddEvent(
long id, string type,
string channelName,
long senderId, string senderName, ChatColour senderColour, int senderRank, string senderNick, ChatUserPermissions senderPerms,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
);
long AddEvent(string type, ChatUser user, ChatChannel channel, object data = null, StoredEventFlags flags = StoredEventFlags.None);
void RemoveEvent(StoredEventInfo evt);
StoredEventInfo GetEvent(long seqId);
IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0);

View file

@ -1,45 +1,13 @@
using MySqlConnector;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Text;
using System.Text.Json;
using System.Threading.Channels;
namespace SharpChat.EventStorage
{
public partial class MariaDBEventStorage : IEventStorage {
private string ConnectionString { get; }
public MariaDBEventStorage(string connString) {
ConnectionString = connString ?? throw new ArgumentNullException(nameof(connString));
}
public void AddEvent(
long id, string type,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
AddEvent(id, type, null, 0, null, ChatColour.None, 0, null, 0, data, flags);
}
public void AddEvent(
long id, string type,
string channelName,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
AddEvent(id, type, channelName, 0, null, ChatColour.None, 0, null, 0, data, flags);
}
public void AddEvent(
long id, string type,
long senderId, string senderName, ChatColour senderColour, int senderRank, string senderNick, ChatUserPermissions senderPerms,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
AddEvent(id, type, null, senderId, senderName, senderColour, senderRank, senderNick, senderPerms, data, flags);
}
public partial class MariaDBEventStorage(string connString) : IEventStorage {
private string ConnectionString { get; } = connString ?? throw new ArgumentNullException(nameof(connString));
public void AddEvent(
long id, string type,
@ -48,8 +16,7 @@ namespace SharpChat.EventStorage
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
if(type == null)
throw new ArgumentNullException(nameof(type));
ArgumentNullException.ThrowIfNull(type);
RunCommand(
"INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`"
@ -71,8 +38,7 @@ namespace SharpChat.EventStorage
}
public long AddEvent(string type, ChatUser user, ChatChannel channel, object data = null, StoredEventFlags flags = StoredEventFlags.None) {
if(type == null)
throw new ArgumentNullException(nameof(type));
ArgumentNullException.ThrowIfNull(type);
long id = SharpId.Next();
@ -137,7 +103,7 @@ namespace SharpChat.EventStorage
}
public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) {
List<StoredEventInfo> events = new();
List<StoredEventInfo> events = [];
try {
using MySqlDataReader reader = RunQuery(
@ -170,8 +136,7 @@ namespace SharpChat.EventStorage
}
public void RemoveEvent(StoredEventInfo evt) {
if(evt == null)
throw new ArgumentNullException(nameof(evt));
ArgumentNullException.ThrowIfNull(evt);
RunCommand(
"UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL",
new MySqlParameter("id", evt.Id)

View file

@ -1,10 +1,8 @@
using System;
namespace SharpChat.EventStorage
{
namespace SharpChat.EventStorage {
[Flags]
public enum StoredEventFlags
{
public enum StoredEventFlags {
None = 0,
Action = 1,
Broadcast = 1 << 1,

View file

@ -2,34 +2,23 @@
using System.Text.Json;
namespace SharpChat.EventStorage {
public class StoredEventInfo {
public long Id { get; set; }
public string Type { get; set; }
public ChatUser Sender { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? Deleted { get; set; }
public string ChannelName { get; set; }
public StoredEventFlags Flags { get; set; }
public JsonDocument Data { get; set; }
public StoredEventInfo(
long id,
string type,
ChatUser sender,
DateTimeOffset created,
DateTimeOffset? deleted,
string channelName,
JsonDocument data,
StoredEventFlags flags
) {
Id = id;
Type = type;
Sender = sender;
Created = created;
Deleted = deleted;
ChannelName = channelName;
Data = data;
Flags = flags;
}
public class StoredEventInfo(
long id,
string type,
ChatUser sender,
DateTimeOffset created,
DateTimeOffset? deleted,
string channelName,
JsonDocument data,
StoredEventFlags flags
) {
public long Id { get; set; } = id;
public string Type { get; set; } = type;
public ChatUser Sender { get; set; } = sender;
public DateTimeOffset Created { get; set; } = created;
public DateTimeOffset? Deleted { get; set; } = deleted;
public string ChannelName { get; set; } = channelName;
public StoredEventFlags Flags { get; set; } = flags;
public JsonDocument Data { get; set; } = data;
}
}

View file

@ -5,33 +5,7 @@ using System.Text.Json;
namespace SharpChat.EventStorage {
public class VirtualEventStorage : IEventStorage {
private readonly Dictionary<long, StoredEventInfo> Events = new();
public void AddEvent(
long id, string type,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
AddEvent(id, type, null, 0, null, ChatColour.None, 0, null, 0, data, flags);
}
public void AddEvent(
long id, string type,
string channelName,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
AddEvent(id, type, channelName, 0, null, ChatColour.None, 0, null, 0, data, flags);
}
public void AddEvent(
long id, string type,
long senderId, string senderName, ChatColour senderColour, int senderRank, string senderNick, ChatUserPermissions senderPerms,
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
AddEvent(id, type, null, senderId, senderName, senderColour, senderRank, senderNick, senderPerms, data, flags);
}
private readonly Dictionary<long, StoredEventInfo> Events = [];
public void AddEvent(
long id, string type,
@ -40,8 +14,7 @@ namespace SharpChat.EventStorage {
object data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
if(type == null)
throw new ArgumentNullException(nameof(type));
ArgumentNullException.ThrowIfNull(type);
// VES is meant as an emergency fallback but this is something else
JsonDocument hack = JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data));
@ -55,35 +28,12 @@ namespace SharpChat.EventStorage {
), DateTimeOffset.Now, null, channelName, hack, flags));
}
public long AddEvent(string type, ChatUser user, ChatChannel channel, object data = null, StoredEventFlags flags = StoredEventFlags.None) {
if(type == null)
throw new ArgumentNullException(nameof(type));
long id = SharpId.Next();
AddEvent(
id, type,
channel?.Name,
user?.UserId ?? 0,
user?.UserName,
user?.Colour ?? ChatColour.None,
user?.Rank ?? 0,
user?.NickName,
user?.Permissions ?? 0,
data,
flags
);
return id;
}
public StoredEventInfo GetEvent(long seqId) {
return Events.TryGetValue(seqId, out StoredEventInfo evt) ? evt : null;
}
public void RemoveEvent(StoredEventInfo evt) {
if(evt == null)
throw new ArgumentNullException(nameof(evt));
ArgumentNullException.ThrowIfNull(evt);
Events.Remove(evt.Id);
}
@ -96,7 +46,7 @@ namespace SharpChat.EventStorage {
start = 0;
}
return subset.Skip(start).Take(amount).ToArray();
return [.. subset.Skip(start).Take(amount)];
}
}
}

View file

@ -1,10 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharpChat.Events {
public interface IChatEvent {
}
namespace SharpChat.Events {
public interface IChatEvent {}
}

View file

@ -1,95 +1,34 @@
using System;
namespace SharpChat.Events {
public class MessageCreateEvent : IChatEvent {
public long MessageId { get; }
public string ChannelName { get; }
public long SenderId { get; }
public string SenderName { get; }
public ChatColour SenderColour { get; }
public int SenderRank { get; }
public string SenderNickName { get; }
public ChatUserPermissions SenderPerms { get; }
public DateTimeOffset MessageCreated { get; }
public class MessageCreateEvent(
long msgId,
string channelName,
long senderId,
string senderName,
ChatColour senderColour,
int senderRank,
string senderNickName,
ChatUserPermissions senderPerms,
DateTimeOffset msgCreated,
string msgText,
bool isPrivate,
bool isAction,
bool isBroadcast
) : IChatEvent {
public long MessageId { get; } = msgId;
public string ChannelName { get; } = channelName;
public long SenderId { get; } = senderId;
public string SenderName { get; } = senderName;
public ChatColour SenderColour { get; } = senderColour;
public int SenderRank { get; } = senderRank;
public string SenderNickName { get; } = senderNickName;
public ChatUserPermissions SenderPerms { get; } = senderPerms;
public DateTimeOffset MessageCreated { get; } = msgCreated;
public string MessageChannel { get; }
public string MessageText { get; }
public bool IsPrivate { get; }
public bool IsAction { get; }
public bool IsBroadcast { get; }
public MessageCreateEvent(
long msgId,
string channelName,
long senderId,
string senderName,
ChatColour senderColour,
int senderRank,
string senderNickName,
ChatUserPermissions senderPerms,
DateTimeOffset msgCreated,
string msgText,
bool isPrivate,
bool isAction,
bool isBroadcast
) {
MessageId = msgId;
ChannelName = channelName;
SenderId = senderId;
SenderName = senderName;
SenderColour = senderColour;
SenderRank = senderRank;
SenderNickName = senderNickName;
SenderPerms = senderPerms;
MessageCreated = msgCreated;
MessageText = msgText;
IsPrivate = isPrivate;
IsAction = isAction;
IsBroadcast = isBroadcast;
}
public MessageCreateEvent(
long msgId,
string channelName,
ChatUser sender,
DateTimeOffset msgCreated,
string msgText,
bool isPrivate,
bool isAction,
bool isBroadcast
) : this(
msgId,
channelName,
sender?.UserId ?? -1,
sender?.UserName ?? null,
sender?.Colour ?? ChatColour.None,
sender?.Rank ?? 0,
sender?.NickName ?? null,
sender?.Permissions ?? 0,
msgCreated,
msgText,
isPrivate,
isAction,
isBroadcast
) { }
public MessageCreateEvent(
long msgId,
ChatChannel channel,
ChatUser sender,
DateTimeOffset msgCreated,
string msgText,
bool isPrivate,
bool isAction,
bool isBroadcast
) : this(
msgId,
channel?.Name ?? null,
sender,
msgCreated,
msgText,
isPrivate,
isAction,
isBroadcast
) { }
public string MessageText { get; } = msgText;
public bool IsPrivate { get; } = isPrivate;
public bool IsAction { get; } = isAction;
public bool IsBroadcast { get; } = isBroadcast;
}
}

View file

@ -6,13 +6,9 @@ namespace SharpChat {
IEnumerable<string> Pack();
}
public abstract class ServerPacket : IServerPacket {
public long SequenceId { get; }
public ServerPacket(long sequenceId = 0) {
// Allow sequence id to be manually set for potential message repeats
SequenceId = sequenceId > 0 ? sequenceId : SharpId.Next();
}
public abstract class ServerPacket(long sequenceId = 0) : IServerPacket {
// Allow sequence id to be manually set for potential message repeats
public long SequenceId { get; } = sequenceId > 0 ? sequenceId : SharpId.Next();
public abstract IEnumerable<string> Pack();
}

View file

@ -25,8 +25,5 @@ namespace SharpChat.Misuzu {
[JsonPropertyName("perms")]
public ChatUserPermissions Permissions { get; set; }
[JsonPropertyName("super")]
public bool IsSuper { get; set; }
}
}

View file

@ -34,8 +34,7 @@ namespace SharpChat.Misuzu {
private CachedValue<string> SecretKey { get; }
public MisuzuClient(HttpClient httpClient, IConfig config) {
if(config == null)
throw new ArgumentNullException(nameof(config));
ArgumentNullException.ThrowIfNull(config);
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
@ -77,8 +76,7 @@ namespace SharpChat.Misuzu {
}
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
if(list == null)
throw new ArgumentNullException(nameof(list));
ArgumentNullException.ThrowIfNull(list);
if(!list.Any())
return;
@ -160,8 +158,7 @@ namespace SharpChat.Misuzu {
}
public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
if(banInfo == null)
throw new ArgumentNullException(nameof(banInfo));
ArgumentNullException.ThrowIfNull(banInfo);
string type = kind switch {
BanRevokeKind.UserId => "user",

View file

@ -3,23 +3,13 @@ using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class AuthSuccessPacket : ServerPacket {
public ChatUser User { get; private set; }
public ChatChannel Channel { get; private set; }
public ChatConnection Connection { get; private set; }
public int MaxMessageLength { get; private set; }
public AuthSuccessPacket(
ChatUser user,
ChatChannel channel,
ChatConnection connection,
int maxMsgLength
) {
User = user ?? throw new ArgumentNullException(nameof(user));
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
MaxMessageLength = maxMsgLength;
}
public class AuthSuccessPacket(
ChatUser user,
ChatChannel channel,
int maxMsgLength
) : ServerPacket {
public ChatUser User { get; private set; } = user ?? throw new ArgumentNullException(nameof(user));
public ChatChannel Channel { get; private set; } = channel ?? throw new ArgumentNullException(nameof(channel));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
@ -30,9 +20,9 @@ namespace SharpChat.Packet {
sb.Append('\t');
sb.Append(Channel.Name);
sb.Append('\t');
sb.Append(MaxMessageLength);
sb.Append(maxMsgLength);
return new[] { sb.ToString() };
yield return sb.ToString();
}
}
}

View file

@ -5,12 +5,8 @@ using System.Linq;
using System.Text;
namespace SharpChat.Packet {
public class BanListPacket : ServerPacket {
public IEnumerable<MisuzuBanInfo> Bans { get; private set; }
public BanListPacket(IEnumerable<MisuzuBanInfo> bans) {
Bans = bans ?? throw new ArgumentNullException(nameof(bans));
}
public class BanListPacket(IEnumerable<MisuzuBanInfo> bans) : ServerPacket {
public IEnumerable<MisuzuBanInfo> Bans { get; private set; } = bans ?? throw new ArgumentNullException(nameof(bans));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
@ -32,7 +28,7 @@ namespace SharpChat.Packet {
sb.Append(SequenceId);
sb.Append("\t10010");
return new[] { sb.ToString() };
yield return sb.ToString();
}
}
}

View file

@ -1,13 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class ChannelCreatePacket : ServerPacket {
public ChatChannel Channel { get; private set; }
public ChannelCreatePacket(ChatChannel channel) {
Channel = channel;
}
public class ChannelCreatePacket(ChatChannel channel) : ServerPacket {
public ChatChannel Channel { get; private set; } = channel ?? throw new ArgumentNullException(nameof(channel));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();

View file

@ -3,12 +3,8 @@ using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class ChannelDeletePacket : ServerPacket {
public ChatChannel Channel { get; private set; }
public ChannelDeletePacket(ChatChannel channel) {
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
}
public class ChannelDeletePacket(ChatChannel channel) : ServerPacket {
public ChatChannel Channel { get; private set; } = channel ?? throw new ArgumentNullException(nameof(channel));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();

View file

@ -1,15 +1,11 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class ChannelUpdatePacket : ServerPacket {
public string PreviousName { get; private set; }
public ChatChannel Channel { get; private set; }
public ChannelUpdatePacket(string previousName, ChatChannel channel) {
PreviousName = previousName;
Channel = channel;
}
public class ChannelUpdatePacket(string previousName, ChatChannel channel) : ServerPacket {
public string PreviousName { get; private set; } = previousName ?? throw new ArgumentNullException(nameof(previousName));
public ChatChannel Channel { get; private set; } = channel ?? throw new ArgumentNullException(nameof(channel));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();

View file

@ -1,46 +1,17 @@
using SharpChat.EventStorage;
using System;
using System;
using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class ChatMessageAddPacket : ServerPacket {
public DateTimeOffset Created { get; }
public long UserId { get; }
public string Text { get; }
public bool IsAction { get; }
public bool IsPrivate { get; }
public ChatMessageAddPacket(
long msgId,
DateTimeOffset created,
long userId,
string text,
bool isAction,
bool isPrivate
) : base(msgId) {
Created = created;
UserId = userId < 0 ? -1 : userId;
Text = text;
IsAction = isAction;
IsPrivate = isPrivate;
}
public static ChatMessageAddPacket FromStoredEvent(StoredEventInfo sei) {
if(sei == null)
throw new ArgumentNullException(nameof(sei));
if(sei.Type is not "msg:add" and not "SharpChat.Events.ChatMessage")
throw new ArgumentException("Wrong event type.", nameof(sei));
return new ChatMessageAddPacket(
sei.Id,
sei.Created,
sei.Sender?.UserId ?? -1,
string.Empty, // todo: this
(sei.Flags & StoredEventFlags.Action) > 0,
(sei.Flags & StoredEventFlags.Private) > 0
);
}
public class ChatMessageAddPacket(
long msgId,
DateTimeOffset created,
long userId,
string text,
bool isAction,
bool isPrivate
) : ServerPacket(msgId) {
public string Text { get; } = text ?? throw new ArgumentNullException(nameof(text));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
@ -48,13 +19,13 @@ namespace SharpChat.Packet {
sb.Append('2');
sb.Append('\t');
sb.Append(Created.ToUnixTimeSeconds());
sb.Append(created.ToUnixTimeSeconds());
sb.Append('\t');
sb.Append(UserId);
sb.Append(userId);
sb.Append('\t');
if(IsAction)
if(isAction)
sb.Append("<i>");
sb.Append(
@ -64,16 +35,16 @@ namespace SharpChat.Packet {
.Replace("\t", " ")
);
if(IsAction)
if(isAction)
sb.Append("</i>");
sb.Append('\t');
sb.Append(SequenceId);
sb.AppendFormat(
"\t1{0}0{1}{2}",
IsAction ? '1' : '0',
IsAction ? '0' : '1',
IsPrivate ? '1' : '0'
isAction ? '1' : '0',
isAction ? '0' : '1',
isPrivate ? '1' : '0'
);
yield return sb.ToString();

View file

@ -2,19 +2,13 @@
using System.Text;
namespace SharpChat.Packet {
public class ChatMessageDeletePacket : ServerPacket {
public long EventId { get; private set; }
public ChatMessageDeletePacket(long eventId) {
EventId = eventId;
}
public class ChatMessageDeletePacket(long eventId) : ServerPacket {
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
sb.Append('6');
sb.Append('\t');
sb.Append(EventId);
sb.Append(eventId);
yield return sb.ToString();
}

View file

@ -4,12 +4,8 @@ using System.Linq;
using System.Text;
namespace SharpChat.Packet {
public class ContextChannelsPacket : ServerPacket {
public IEnumerable<ChatChannel> Channels { get; private set; }
public ContextChannelsPacket(IEnumerable<ChatChannel> channels) {
Channels = channels?.Where(c => c != null) ?? throw new ArgumentNullException(nameof(channels));
}
public class ContextChannelsPacket(IEnumerable<ChatChannel> channels) : ServerPacket {
public IEnumerable<ChatChannel> Channels { get; private set; } = channels?.Where(c => c != null) ?? throw new ArgumentNullException(nameof(channels));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();

View file

@ -10,24 +10,13 @@ namespace SharpChat.Packet {
MessagesUsersChannels = 4,
}
public class ContextClearPacket : ServerPacket {
public ChatChannel Channel { get; private set; }
public ContextClearMode Mode { get; private set; }
public bool IsGlobal
=> Channel == null;
public ContextClearPacket(ChatChannel channel, ContextClearMode mode) {
Channel = channel;
Mode = mode;
}
public class ContextClearPacket(ContextClearMode mode) : ServerPacket {
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
sb.Append('8');
sb.Append('\t');
sb.Append((int)Mode);
sb.Append((int)mode);
yield return sb.ToString();
}

View file

@ -5,14 +5,8 @@ using System.Text;
namespace SharpChat.Packet
{
public class ContextMessagePacket : ServerPacket {
public StoredEventInfo Event { get; private set; }
public bool Notify { get; private set; }
public ContextMessagePacket(StoredEventInfo evt, bool notify = false) {
Event = evt ?? throw new ArgumentNullException(nameof(evt));
Notify = notify;
}
public class ContextMessagePacket(StoredEventInfo evt, bool notify = false) : ServerPacket {
public StoredEventInfo Event { get; private set; } = evt ?? throw new ArgumentNullException(nameof(evt));
private const string V1_CHATBOT = "-1\tChatBot\tinherit\t\t";
@ -106,7 +100,7 @@ namespace SharpChat.Packet
sb.Append('\t');
sb.Append(Event.Id < 1 ? SequenceId : Event.Id);
sb.Append('\t');
sb.Append(Notify ? '1' : '0');
sb.Append(notify ? '1' : '0');
sb.AppendFormat(
"\t1{0}0{1}{2}",
isAction ? '1' : '0',

View file

@ -4,12 +4,8 @@ using System.Linq;
using System.Text;
namespace SharpChat.Packet {
public class ContextUsersPacket : ServerPacket {
public IEnumerable<ChatUser> Users { get; private set; }
public ContextUsersPacket(IEnumerable<ChatUser> users) {
Users = users?.Where(u => u != null) ?? throw new ArgumentNullException(nameof(users));
}
public class ContextUsersPacket(IEnumerable<ChatUser> users) : ServerPacket {
public IEnumerable<ChatUser> Users { get; private set; } = users?.Where(u => u != null) ?? throw new ArgumentNullException(nameof(users));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();

View file

@ -1,23 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SharpChat.Packet {
public class LegacyCommandResponse : ServerPacket {
public bool IsError { get; private set; }
public string StringId { get; private set; }
public IEnumerable<object> Arguments { get; private set; }
public LegacyCommandResponse(
string stringId,
bool isError = true,
params object[] args
) {
IsError = isError;
StringId = stringId;
Arguments = args;
}
public class LegacyCommandResponse(
string stringId,
bool isError = true,
params object[] args
) : ServerPacket {
public string StringId { get; private set; } = stringId ?? throw new ArgumentNullException(nameof(stringId));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
@ -36,12 +27,12 @@ namespace SharpChat.Packet {
sb.Append("\t-1\t");
}
sb.Append(IsError ? '1' : '0');
sb.Append(isError ? '1' : '0');
sb.Append('\f');
sb.Append(StringId == LCR.WELCOME ? LCR.BROADCAST : StringId);
if(Arguments?.Any() == true)
foreach(object arg in Arguments) {
if(args.Length > 0)
foreach(object arg in args) {
sb.Append('\f');
sb.Append(arg);
}

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;
namespace SharpChat.Packet {
public class PongPacket : ServerPacket {

View file

@ -3,12 +3,8 @@ using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class UserChannelForceJoinPacket : ServerPacket {
public ChatChannel Channel { get; private set; }
public UserChannelForceJoinPacket(ChatChannel channel) {
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
}
public class UserChannelForceJoinPacket(ChatChannel channel) : ServerPacket {
public ChatChannel Channel { get; private set; } = channel ?? throw new ArgumentNullException(nameof(channel));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();

View file

@ -3,12 +3,8 @@ using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class UserChannelJoinPacket : ServerPacket {
public ChatUser User { get; private set; }
public UserChannelJoinPacket(ChatUser user) {
User = user ?? throw new ArgumentNullException(nameof(user));
}
public class UserChannelJoinPacket(ChatUser user) : ServerPacket {
public ChatUser User { get; private set; } = user ?? throw new ArgumentNullException(nameof(user));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
@ -21,7 +17,7 @@ namespace SharpChat.Packet {
sb.Append('\t');
sb.Append(SequenceId);
return new[] { sb.ToString() };
yield return sb.ToString();
}
}
}

View file

@ -3,12 +3,8 @@ using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class UserChannelLeavePacket : ServerPacket {
public ChatUser User { get; private set; }
public UserChannelLeavePacket(ChatUser user) {
User = user ?? throw new ArgumentNullException(nameof(user));
}
public class UserChannelLeavePacket(ChatUser user) : ServerPacket {
public ChatUser User { get; private set; } = user ?? throw new ArgumentNullException(nameof(user));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();

View file

@ -3,21 +3,15 @@ using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class UserConnectPacket : ServerPacket {
public DateTimeOffset Joined { get; private set; }
public ChatUser User { get; private set; }
public UserConnectPacket(DateTimeOffset joined, ChatUser user) {
Joined = joined;
User = user ?? throw new ArgumentNullException(nameof(user));
}
public class UserConnectPacket(DateTimeOffset joined, ChatUser user) : ServerPacket {
public ChatUser User { get; private set; } = user ?? throw new ArgumentNullException(nameof(user));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
sb.Append('1');
sb.Append('\t');
sb.Append(Joined.ToUnixTimeSeconds());
sb.Append(joined.ToUnixTimeSeconds());
sb.Append('\t');
sb.Append(User.Pack());
sb.Append('\t');

View file

@ -10,16 +10,8 @@ namespace SharpChat.Packet {
Flood,
}
public class UserDisconnectPacket : ServerPacket {
public DateTimeOffset Disconnected { get; private set; }
public ChatUser User { get; private set; }
public UserDisconnectReason Reason { get; private set; }
public UserDisconnectPacket(DateTimeOffset disconnected, ChatUser user, UserDisconnectReason reason) {
Disconnected = disconnected;
User = user ?? throw new ArgumentNullException(nameof(user));
Reason = reason;
}
public class UserDisconnectPacket(DateTimeOffset disconnected, ChatUser user, UserDisconnectReason reason) : ServerPacket {
public ChatUser User { get; private set; } = user ?? throw new ArgumentNullException(nameof(user));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
@ -31,7 +23,7 @@ namespace SharpChat.Packet {
sb.Append(User.LegacyNameWithStatus);
sb.Append('\t');
switch(Reason) {
switch(reason) {
case UserDisconnectReason.Leave:
default:
sb.Append("leave");
@ -48,11 +40,11 @@ namespace SharpChat.Packet {
}
sb.Append('\t');
sb.Append(Disconnected.ToUnixTimeSeconds());
sb.Append(disconnected.ToUnixTimeSeconds());
sb.Append('\t');
sb.Append(SequenceId);
return new[] { sb.ToString() };
yield return sb.ToString();
}
}
}

View file

@ -3,26 +3,20 @@ using System.Collections.Generic;
using System.Text;
namespace SharpChat.Packet {
public class UserUpdatePacket : ServerPacket {
public ChatUser User { get; private set; }
public string PreviousName { get; private set; }
public UserUpdatePacket(ChatUser user, string previousName = null) {
User = user ?? throw new ArgumentNullException(nameof(user));
PreviousName = previousName;
}
public class UserUpdatePacket(ChatUser user, string previousName = "") : ServerPacket {
public ChatUser User { get; private set; } = user ?? throw new ArgumentNullException(nameof(user));
public override IEnumerable<string> Pack() {
StringBuilder sb = new();
bool isSilent = string.IsNullOrEmpty(PreviousName);
bool isSilent = string.IsNullOrEmpty(previousName);
if(!isSilent) {
sb.Append('2');
sb.Append('\t');
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
sb.Append("\t-1\t0\fnick\f");
sb.Append(PreviousName);
sb.Append(previousName);
sb.Append('\f');
sb.Append(User.LegacyNameWithStatus);
sb.Append('\t');

View file

@ -8,23 +8,16 @@ using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.PacketHandlers {
public class AuthHandler : IChatPacketHandler {
private readonly MisuzuClient Misuzu;
private readonly ChatChannel DefaultChannel;
private readonly CachedValue<int> MaxMessageLength;
private readonly CachedValue<int> MaxConnections;
public AuthHandler(
MisuzuClient msz,
ChatChannel defaultChannel,
CachedValue<int> maxMsgLength,
CachedValue<int> maxConns
) {
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns));
}
public class AuthHandler(
MisuzuClient msz,
ChatChannel defaultChannel,
CachedValue<int> maxMsgLength,
CachedValue<int> maxConns
) : IChatPacketHandler {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
private readonly ChatChannel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns));
public bool IsMatch(ChatPacketHandlerContext ctx) {
return ctx.CheckPacketId("1");
@ -108,8 +101,7 @@ namespace SharpChat.PacketHandlers {
fai.UserName,
fai.Colour,
fai.Rank,
fai.Permissions,
isSuper: fai.IsSuper
fai.Permissions
);
else
ctx.Chat.UpdateUser(
@ -117,8 +109,7 @@ namespace SharpChat.PacketHandlers {
userName: fai.UserName,
colour: fai.Colour,
rank: fai.Rank,
perms: fai.Permissions,
isSuper: fai.IsSuper
perms: fai.Permissions
);
// Enforce a maximum amount of connections per user

View file

@ -5,16 +5,12 @@ using System.Linq;
using System.Threading.Tasks;
namespace SharpChat.PacketHandlers {
public class PingHandler : IChatPacketHandler {
private readonly MisuzuClient Misuzu;
public class PingHandler(MisuzuClient msz) : IChatPacketHandler {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1);
private DateTimeOffset LastBump = DateTimeOffset.MinValue;
public PingHandler(MisuzuClient msz) {
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
}
public bool IsMatch(ChatPacketHandlerContext ctx) {
return ctx.CheckPacketId("0");
}
@ -31,12 +27,11 @@ namespace SharpChat.PacketHandlers {
ctx.Chat.ContextAccess.Wait();
try {
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
(string, string)[] bumpList = ctx.Chat.Users
(string, string)[] bumpList = [.. ctx.Chat.Users
.Where(u => u.Status == ChatUserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u))
.Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty))
.ToArray();
.Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty))];
if(bumpList.Any())
if(bumpList.Length > 0)
Task.Run(async () => {
await Misuzu.BumpUsersOnlineAsync(bumpList);
}).Wait();

View file

@ -1,24 +1,16 @@
using SharpChat.Commands;
using SharpChat.Config;
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Packet;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace SharpChat.PacketHandlers
{
public class SendMessageHandler : IChatPacketHandler {
private readonly CachedValue<int> MaxMessageLength;
namespace SharpChat.PacketHandlers {
public class SendMessageHandler(CachedValue<int> maxMsgLength) : IChatPacketHandler {
private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
private List<IChatCommand> Commands { get; } = new();
public SendMessageHandler(CachedValue<int> maxMsgLength) {
MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
}
private List<IChatCommand> Commands { get; } = [];
public void AddCommand(IChatCommand command) {
Commands.Add(command ?? throw new ArgumentNullException(nameof(command)));
@ -57,7 +49,7 @@ namespace SharpChat.PacketHandlers
ctx.Chat.UpdateUser(user, status: ChatUserStatus.Online);
int maxMsgLength = MaxMessageLength;
StringInfo messageTextInfo = new StringInfo(messageText);
StringInfo messageTextInfo = new(messageText);
if(Encoding.UTF8.GetByteCount(messageText) > (maxMsgLength * 10)
|| messageTextInfo.LengthInTextElements > maxMsgLength)
messageText = messageTextInfo.SubstringByTextElements(0, Math.Min(messageTextInfo.LengthInTextElements, maxMsgLength));
@ -68,7 +60,7 @@ namespace SharpChat.PacketHandlers
Logger.Write($"<{ctx.Connection.Id} {user.UserName}> {messageText}");
#endif
if(messageText.StartsWith("/")) {
if(messageText.StartsWith('/')) {
ChatCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channel);
IChatCommand command = null;
@ -87,8 +79,13 @@ namespace SharpChat.PacketHandlers
ctx.Chat.DispatchEvent(new MessageCreateEvent(
SharpId.Next(),
channel,
user,
channel.Name,
user.UserId,
user.UserName,
user.Colour,
user.Rank,
user.NickName,
user.Permissions,
DateTimeOffset.Now,
messageText,
false, false, false

View file

@ -45,7 +45,7 @@ namespace SharpChat {
if(!File.Exists(configFile) && configFile == CONFIG)
ConvertConfiguration();
using IConfig config = new StreamConfig(configFile);
using StreamConfig config = StreamConfig.FromPath(configFile);
if(hasCancelled) return;
@ -124,7 +124,7 @@ namespace SharpChat {
const string mdb_config = @"mariadb.txt";
string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : Array.Empty<string>();
string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : [];
sw.WriteLine();
sw.WriteLine("# MariaDB configuration");

View file

@ -2,12 +2,20 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fleck" Version="1.2.0" />
<PackageReference Include="MySqlConnector" Version="2.2.5" />
<PackageReference Include="MySqlConnector" Version="2.4.0" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View file

@ -37,7 +37,7 @@ namespace SharpChat {
}
ListenerSocket = new SocketWrapper(socket);
SupportedSubProtocols = Array.Empty<string>();
SupportedSubProtocols = [];
}
public ISocket ListenerSocket { get; set; }

View file

@ -30,12 +30,14 @@ namespace SharpChat {
private readonly CachedValue<int> FloodKickLength;
private readonly CachedValue<int> FloodKickExemptRank;
private readonly List<IChatPacketHandler> GuestHandlers = new();
private readonly List<IChatPacketHandler> AuthedHandlers = new();
private readonly List<IChatPacketHandler> GuestHandlers = [];
private readonly List<IChatPacketHandler> AuthedHandlers = [];
private readonly SendMessageHandler SendMessageHandler;
private bool IsShuttingDown = false;
private static readonly string[] DEFAULT_CHANNELS = ["lounge"];
private ChatChannel DefaultChannel { get; set; }
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IEventStorage evtStore, IConfig config) {
@ -51,7 +53,7 @@ namespace SharpChat {
Context = new ChatContext(evtStore);
string[] channelNames = config.ReadValue("channels", new[] { "lounge" });
string[] channelNames = config.ReadValue("channels", DEFAULT_CHANNELS);
foreach(string channelName in channelNames) {
IConfig channelCfg = config.ScopeTo($"channels:{channelName}");
@ -72,12 +74,12 @@ namespace SharpChat {
GuestHandlers.Add(new AuthHandler(Misuzu, DefaultChannel, MaxMessageLength, MaxConnections));
AuthedHandlers.AddRange(new IChatPacketHandler[] {
AuthedHandlers.AddRange([
new PingHandler(Misuzu),
SendMessageHandler = new SendMessageHandler(MaxMessageLength),
});
]);
SendMessageHandler.AddCommands(new IChatCommand[] {
SendMessageHandler.AddCommands([
new AFKCommand(),
new NickCommand(),
new WhisperCommand(),
@ -95,7 +97,7 @@ namespace SharpChat {
new PardonAddressCommand(msz),
new BanListCommand(msz),
new RemoteAddressCommand(),
});
]);
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}");