Beginnings of moving channel handling out of the main context or random assignments.

This commit is contained in:
flash 2025-04-27 02:53:56 +00:00
parent f1d4051fb5
commit 80475a9180
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
17 changed files with 209 additions and 78 deletions

View file

@ -1,5 +1,6 @@
using SharpChat.Auth; using SharpChat.Auth;
using SharpChat.Bans; using SharpChat.Bans;
using SharpChat.Channels;
using SharpChat.Configuration; using SharpChat.Configuration;
using SharpChat.SockChat.S2CPackets; using SharpChat.SockChat.S2CPackets;
@ -8,14 +9,10 @@ namespace SharpChat.C2SPacketHandlers;
public class AuthC2SPacketHandler( public class AuthC2SPacketHandler(
AuthClient authClient, AuthClient authClient,
BansClient bansClient, BansClient bansClient,
Channel defaultChannel, ChannelsContext channelsCtx,
CachedValue<int> maxMsgLength, CachedValue<int> maxMsgLength,
CachedValue<int> maxConns CachedValue<int> maxConns
) : C2SPacketHandler { ) : C2SPacketHandler {
private readonly Channel 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(C2SPacketHandlerContext ctx) { public bool IsMatch(C2SPacketHandlerContext ctx) {
return ctx.CheckPacketId("1"); return ctx.CheckPacketId("1");
} }
@ -75,7 +72,7 @@ public class AuthC2SPacketHandler(
); );
// Enforce a maximum amount of connections per user // Enforce a maximum amount of connections per user
if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { if(ctx.Chat.Connections.Count(conn => conn.User == user) >= maxConns) {
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions)); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions));
ctx.Connection.Dispose(); ctx.Connection.Dispose();
return; return;
@ -93,7 +90,7 @@ public class AuthC2SPacketHandler(
await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line)); await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line));
} }
await ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); await ctx.Chat.HandleJoin(user, channelsCtx.DefaultChannel, ctx.Connection, maxMsgLength);
} finally { } finally {
ctx.Chat.ContextAccess.Release(); ctx.Chat.ContextAccess.Release();
} }

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.Configuration; using SharpChat.Configuration;
using SharpChat.Events; using SharpChat.Events;
using SharpChat.Snowflake; using SharpChat.Snowflake;

View file

@ -1,4 +1,6 @@
namespace SharpChat; using SharpChat.Channels;
namespace SharpChat;
public class ClientCommandContext { public class ClientCommandContext {
public string Name { get; } public string Name { get; }

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.SockChat.S2CPackets; using SharpChat.SockChat.S2CPackets;
namespace SharpChat.ClientCommands; namespace SharpChat.ClientCommands;
@ -35,28 +36,23 @@ public class CreateChannelClientCommand : ClientCommand {
string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0)); string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0));
if(!Channel.CheckName(createChanName)) { try {
Channel channel = ctx.Chat.Channels.CreateChannel(
createChanName,
temporary: !ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPermanent),
rank: createChanHierarchy,
ownerId: ctx.User.UserId
);
foreach(User ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank))
await ctx.Chat.SendTo(ccu, new ChannelCreateS2CPacket(channel.Name, channel.HasPassword, channel.IsTemporary));
await ctx.Chat.SwitchChannel(ctx.User, channel, channel.Password);
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_CREATED, false, channel.Name));
} catch(ChannelNameFormatException) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NAME_INVALID)); await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NAME_INVALID));
return; } catch(ChannelExistsException) {
}
if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_ALREADY_EXISTS, true, createChanName));
return;
} }
Channel createChan = new(
createChanName,
isTemporary: !ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPermanent),
rank: createChanHierarchy,
ownerId: ctx.User.UserId
);
ctx.Chat.Channels.Add(createChan);
foreach(User ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank))
await ctx.Chat.SendTo(ccu, new ChannelCreateS2CPacket(createChan.Name, createChan.HasPassword, createChan.IsTemporary));
await ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password);
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_CREATED, false, createChan.Name));
} }
} }

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.SockChat.S2CPackets; using SharpChat.SockChat.S2CPackets;
namespace SharpChat.ClientCommands; namespace SharpChat.ClientCommands;
@ -19,7 +20,7 @@ public class DeleteChannelClientCommand : ClientCommand {
} }
string delChanName = string.Join('_', ctx.Args); string delChanName = string.Join('_', ctx.Args);
Channel? delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName)); Channel? delChan = ctx.Chat.Channels.GetChannel(delChanName);
if(delChan == null) { if(delChan == null) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, delChanName)); await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, delChanName));

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.SockChat.S2CPackets; using SharpChat.SockChat.S2CPackets;
namespace SharpChat.ClientCommands; namespace SharpChat.ClientCommands;
@ -10,7 +11,7 @@ public class JoinChannelClientCommand : ClientCommand {
public async Task Dispatch(ClientCommandContext ctx) { public async Task Dispatch(ClientCommandContext ctx) {
long msgId = ctx.Chat.RandomSnowflake.Next(); long msgId = ctx.Chat.RandomSnowflake.Next();
string joinChanStr = ctx.Args.FirstOrDefault() ?? "Channel"; string joinChanStr = ctx.Args.FirstOrDefault() ?? "Channel";
Channel? joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); Channel? joinChan = ctx.Chat.Channels.GetChannel(joinChanStr);
if(joinChan is null) { if(joinChan is null) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, joinChanStr));

View file

@ -22,7 +22,7 @@ public class RankChannelClientCommand : ClientCommand {
return; return;
} }
await ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy); await ctx.Chat.UpdateChannel(ctx.Channel, rank: chanHierarchy);
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_HIERARCHY_CHANGED, false)); await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_HIERARCHY_CHANGED, false));
} }
} }

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.SockChat.S2CPackets; using SharpChat.SockChat.S2CPackets;
using System.Text; using System.Text;
@ -30,7 +31,7 @@ public class WhoClientCommand : ClientCommand {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_SERVER, false, whoChanSB)); await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_SERVER, false, whoChanSB));
} else { } else {
Channel? whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr)); Channel? whoChan = ctx.Chat.Channels.GetChannel(whoChanStr);
if(whoChan is null) { if(whoChan is null) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, whoChanStr));

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.Events; using SharpChat.Events;
using SharpChat.Messages; using SharpChat.Messages;
using SharpChat.Snowflake; using SharpChat.Snowflake;
@ -15,7 +16,7 @@ public class Context {
public SnowflakeGenerator SnowflakeGenerator { get; } = new(); public SnowflakeGenerator SnowflakeGenerator { get; } = new();
public RandomSnowflake RandomSnowflake { get; } public RandomSnowflake RandomSnowflake { get; }
public HashSet<Channel> Channels { get; } = []; public ChannelsContext Channels { get; } = new();
public HashSet<Connection> Connections { get; } = []; public HashSet<Connection> Connections { get; } = [];
public HashSet<User> Users { get; } = []; public HashSet<User> Users { get; } = [];
public MessageStorage Messages { get; } public MessageStorage Messages { get; }
@ -59,7 +60,7 @@ public class Context {
true true
)); ));
} else { } else {
Channel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName)); Channel? channel = Channels.GetChannel(mce.ChannelName);
if(channel is not null) if(channel is not null)
await SendTo(channel, new ChatMessageAddS2CPacket( await SendTo(channel, new ChatMessageAddS2CPacket(
mce.MessageId, mce.MessageId,
@ -118,8 +119,7 @@ public class Context {
} }
public Channel[] GetUserChannels(User user) { public Channel[] GetUserChannels(User user) {
string[] names = GetUserChannelNames(user); return [.. Channels.GetChannels(GetUserChannelNames(user))];
return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
} }
public string[] GetChannelUserIds(Channel channel) { public string[] GetChannelUserIds(Channel channel) {
@ -241,7 +241,7 @@ public class Context {
await conn.Send(new ContextMessageS2CPacket(msg)); await conn.Send(new ContextMessageS2CPacket(msg));
await conn.Send(new ContextChannelsS2CPacket( await conn.Send(new ContextChannelsS2CPacket(
Channels.Where(c => c.Rank <= user.Rank) Channels.GetChannels(user.Rank)
.Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary)) .Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary))
)); ));
@ -294,9 +294,6 @@ public class Context {
} }
public async Task ForceChannelSwitch(User user, Channel chan) { public async Task ForceChannelSwitch(User user, Channel chan) {
if(!Channels.Any(c => c.NameEquals(chan.Name)))
return;
Channel oldChan = UserLastChannel[user.UserId]; Channel oldChan = UserLastChannel[user.UserId];
long leaveId = RandomSnowflake.Next(); long leaveId = RandomSnowflake.Next();
@ -354,7 +351,7 @@ public class Context {
} }
public async Task SendToUserChannels(User user, S2CPacket packet) { public async Task SendToUserChannels(User user, S2CPacket packet) {
IEnumerable<Channel> chans = Channels.Where(c => IsInChannel(user, c)); IEnumerable<Channel> chans = Channels.GetChannels(c => IsInChannel(user, c));
IEnumerable<Connection> conns = Connections.Where(conn => conn.IsAlive && conn.User is not null && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName)))); IEnumerable<Connection> conns = Connections.Where(conn => conn.IsAlive && conn.User is not null && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName))));
foreach(Connection conn in conns) foreach(Connection conn in conns)
await conn.Send(packet); await conn.Send(packet);
@ -373,18 +370,18 @@ public class Context {
await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name)); await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name));
} }
public async Task UpdateChannel(Channel channel, bool? temporary = null, int? hierarchy = null, string? password = null) { public async Task UpdateChannel(
if(!Channels.Any(c => c.NameEquals(channel.Name))) Channel channel,
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel)); bool? temporary = null,
int? rank = null,
if(temporary.HasValue) string? password = null
channel.IsTemporary = temporary.Value; ) {
Channels.UpdateChannel(
if(hierarchy.HasValue) channel,
channel.Rank = hierarchy.Value; temporary: temporary,
rank: rank,
if(password != null) password: password
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 // 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(User user in Users.Where(u => u.Rank >= channel.Rank)) foreach(User user in Users.Where(u => u.Rank >= channel.Rank))
@ -392,20 +389,13 @@ public class Context {
} }
public async Task RemoveChannel(Channel channel) { public async Task RemoveChannel(Channel channel) {
if(channel == null || Channels.Count < 1)
return;
Channel? defaultChannel = Channels.FirstOrDefault();
if(defaultChannel is null)
return;
// Remove channel from the listing // Remove channel from the listing
Channels.Remove(channel); Channels.RemoveChannel(channel.Name);
// Move all users back to the main 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. // TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel.
foreach(User user in GetChannelUsers(channel)) foreach(User user in GetChannelUsers(channel))
await SwitchChannel(user, defaultChannel, string.Empty); await SwitchChannel(user, Channels.DefaultChannel, string.Empty);
// Broadcast deletion of channel // Broadcast deletion of channel
foreach(User user in Users.Where(u => u.Rank >= channel.Rank)) foreach(User user in Users.Where(u => u.Rank >= channel.Rank))

View file

@ -2,6 +2,7 @@ using Fleck;
using SharpChat.Auth; using SharpChat.Auth;
using SharpChat.Bans; using SharpChat.Bans;
using SharpChat.C2SPacketHandlers; using SharpChat.C2SPacketHandlers;
using SharpChat.Channels;
using SharpChat.ClientCommands; using SharpChat.ClientCommands;
using SharpChat.Configuration; using SharpChat.Configuration;
using SharpChat.Messages; using SharpChat.Messages;
@ -35,8 +36,6 @@ public class SockChatServer : IDisposable {
private static readonly string[] DEFAULT_CHANNELS = ["lounge"]; private static readonly string[] DEFAULT_CHANNELS = ["lounge"];
private Channel DefaultChannel { get; set; }
public SockChatServer( public SockChatServer(
AuthClient authClient, AuthClient authClient,
BansClient bansClient, BansClient bansClient,
@ -63,20 +62,14 @@ public class SockChatServer : IDisposable {
if(string.IsNullOrWhiteSpace(name)) if(string.IsNullOrWhiteSpace(name))
name = channelName; name = channelName;
Channel channelInfo = new( Context.Channels.CreateChannel(
name, name,
channelCfg.SafeReadValue("password", string.Empty)!, channelCfg.SafeReadValue("password", string.Empty)!,
rank: channelCfg.SafeReadValue("minRank", 0) rank: channelCfg.SafeReadValue("minRank", 0)
); );
Context.Channels.Add(channelInfo);
DefaultChannel ??= channelInfo;
} }
if(DefaultChannel is null) GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, Context.Channels, MaxMessageLength, MaxConnections));
throw new Exception("The default channel could not be determined.");
GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections));
AuthedHandlers.AddRange([ AuthedHandlers.AddRange([
new PingC2SPacketHandler(authClient), new PingC2SPacketHandler(authClient),

View file

@ -1,4 +1,4 @@
namespace SharpChat; namespace SharpChat.Channels;
public class Channel( public class Channel(
string name, string name,
@ -7,15 +7,18 @@ public class Channel(
int rank = 0, int rank = 0,
string ownerId = "" string ownerId = ""
) { ) {
public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); public string Name { get; internal set; } = name;
public string Password { get; set; } = password ?? string.Empty; public string Password { get; internal set; } = password ?? string.Empty;
public bool IsTemporary { get; set; } = isTemporary; public bool IsTemporary { get; internal set; } = isTemporary;
public int Rank { get; set; } = rank; public int Rank { get; internal set; } = rank;
public string OwnerId { get; set; } = ownerId; public string OwnerId { get; internal set; } = ownerId;
public bool HasPassword public bool HasPassword
=> !string.IsNullOrWhiteSpace(Password); => !string.IsNullOrWhiteSpace(Password);
public bool IsPublic
=> !HasPassword && Rank < 1;
public bool NameEquals(string name) { public bool NameEquals(string name) {
return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase); return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase);
} }

View file

@ -0,0 +1,4 @@
namespace SharpChat.Channels;
public class ChannelExistsException(string argName)
: ArgumentException("A channel with that name already exists.", argName) {}

View file

@ -0,0 +1,4 @@
namespace SharpChat.Channels;
public class ChannelIsDefaultException(string argName)
: ArgumentException("You cannot delete the default channel.", argName) {}

View file

@ -0,0 +1,4 @@
namespace SharpChat.Channels;
public class ChannelNameFormatException(string argName)
: ArgumentException("Channel name contains unsupported characters.", argName) { }

View file

@ -0,0 +1,4 @@
namespace SharpChat.Channels;
public class ChannelNotFoundException(string argName)
: ArgumentException("No channel with that name exists.", argName) {}

View file

@ -0,0 +1,126 @@
namespace SharpChat.Channels;
public class ChannelsContext {
private readonly List<Channel> Channels = [];
public int Count => Channels.Count;
private Channel? DefaultChannelValue;
public Channel DefaultChannel {
get {
if(DefaultChannelValue is not null) {
if(Channels.Contains(DefaultChannelValue))
return DefaultChannelValue;
DefaultChannelValue = null;
}
return GetChannel(c => c.IsPublic && !c.IsTemporary) ?? throw new NoDefaultChannelException();
}
set => DefaultChannelValue = value;
}
public bool ChannelExists(Func<Channel, bool> predicate) {
return Channels.Any(predicate);
}
public bool ChannelExists(string name) {
return ChannelExists(c => c.NameEquals(name));
}
public Channel? GetChannel(Func<Channel, bool> predicate) {
return Channels.FirstOrDefault(predicate);
}
public Channel? GetChannel(string name) {
return GetChannel(c => c.NameEquals(name));
}
public IEnumerable<Channel> GetChannels(Func<Channel, bool> predicate) {
return Channels.Where(predicate);
}
public IEnumerable<Channel> GetChannels(IEnumerable<string> names) {
return GetChannels(c => names.Any(n => c.NameEquals(n)));
}
public IEnumerable<Channel> GetChannels(int maxRank) {
return GetChannels(c => c.Rank <= maxRank);
}
public Channel CreateChannel(
string name,
string password = "",
bool temporary = false,
int rank = 0,
string ownerId = ""
) {
if(!Channel.CheckName(name))
throw new ChannelNameFormatException(nameof(name));
if(ChannelExists(name))
throw new ChannelExistsException(nameof(name));
Channel channel = new(
name,
password ?? string.Empty,
temporary,
rank,
ownerId ?? string.Empty
);
Channels.Add(channel);
return channel;
}
public Channel UpdateChannel(
Channel channel,
string? name = null,
string? password = null,
bool? temporary = null,
int? rank = null,
string? ownerId = null
) => UpdateChannel(channel.Name, name, password, temporary, rank, ownerId);
public Channel UpdateChannel(
string currentName,
string? name = null,
string? password = null,
bool? temporary = null,
int? rank = null,
string? ownerId = null
) {
Channel channel = GetChannel(currentName) ?? throw new ChannelNotFoundException(nameof(currentName));
if(name is not null && currentName != name) {
if(!Channel.CheckName(name))
throw new ChannelNameFormatException(nameof(name));
if(ChannelExists(name))
throw new ChannelExistsException(nameof(name));
}
if(name is not null)
channel.Name = name;
if(password is not null)
channel.Password = password;
if(temporary.HasValue)
channel.IsTemporary = temporary.Value;
if(rank.HasValue)
channel.Rank = rank.Value;
if(ownerId is not null)
channel.OwnerId = ownerId;
return channel;
}
public void RemoveChannel(string name) {
Channel channel = GetChannel(name) ?? throw new ChannelNotFoundException(nameof(name));
Channel defaultChannel = DefaultChannel;
if(channel == defaultChannel || defaultChannel.NameEquals(channel.Name))
throw new ChannelIsDefaultException(nameof(name));
Channels.Remove(channel);
}
}

View file

@ -0,0 +1,4 @@
namespace SharpChat.Channels;
public class NoDefaultChannelException()
: NullReferenceException("A public, non-temporary default channel could not be determined.") {}