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.Bans;
using SharpChat.Channels;
using SharpChat.Configuration;
using SharpChat.SockChat.S2CPackets;
@ -8,14 +9,10 @@ namespace SharpChat.C2SPacketHandlers;
public class AuthC2SPacketHandler(
AuthClient authClient,
BansClient bansClient,
Channel defaultChannel,
ChannelsContext channelsCtx,
CachedValue<int> maxMsgLength,
CachedValue<int> maxConns
) : 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) {
return ctx.CheckPacketId("1");
}
@ -75,7 +72,7 @@ public class AuthC2SPacketHandler(
);
// 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));
ctx.Connection.Dispose();
return;
@ -93,7 +90,7 @@ public class AuthC2SPacketHandler(
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 {
ctx.Chat.ContextAccess.Release();
}

View file

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

View file

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

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.SockChat.S2CPackets;
namespace SharpChat.ClientCommands;
@ -35,28 +36,23 @@ public class CreateChannelClientCommand : ClientCommand {
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));
return;
}
if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) {
} catch(ChannelExistsException) {
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;
namespace SharpChat.ClientCommands;
@ -19,7 +20,7 @@ public class DeleteChannelClientCommand : ClientCommand {
}
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) {
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;
namespace SharpChat.ClientCommands;
@ -10,7 +11,7 @@ public class JoinChannelClientCommand : ClientCommand {
public async Task Dispatch(ClientCommandContext ctx) {
long msgId = ctx.Chat.RandomSnowflake.Next();
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) {
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;
}
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));
}
}

View file

@ -1,3 +1,4 @@
using SharpChat.Channels;
using SharpChat.SockChat.S2CPackets;
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));
} else {
Channel? whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr));
Channel? whoChan = ctx.Chat.Channels.GetChannel(whoChanStr);
if(whoChan is null) {
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.Messages;
using SharpChat.Snowflake;
@ -15,7 +16,7 @@ public class Context {
public SnowflakeGenerator SnowflakeGenerator { get; } = new();
public RandomSnowflake RandomSnowflake { get; }
public HashSet<Channel> Channels { get; } = [];
public ChannelsContext Channels { get; } = new();
public HashSet<Connection> Connections { get; } = [];
public HashSet<User> Users { get; } = [];
public MessageStorage Messages { get; }
@ -59,7 +60,7 @@ public class Context {
true
));
} else {
Channel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
Channel? channel = Channels.GetChannel(mce.ChannelName);
if(channel is not null)
await SendTo(channel, new ChatMessageAddS2CPacket(
mce.MessageId,
@ -118,8 +119,7 @@ public class Context {
}
public Channel[] GetUserChannels(User user) {
string[] names = GetUserChannelNames(user);
return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
return [.. Channels.GetChannels(GetUserChannelNames(user))];
}
public string[] GetChannelUserIds(Channel channel) {
@ -241,7 +241,7 @@ public class Context {
await conn.Send(new ContextMessageS2CPacket(msg));
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))
));
@ -294,9 +294,6 @@ public class Context {
}
public async Task ForceChannelSwitch(User user, Channel chan) {
if(!Channels.Any(c => c.NameEquals(chan.Name)))
return;
Channel oldChan = UserLastChannel[user.UserId];
long leaveId = RandomSnowflake.Next();
@ -354,7 +351,7 @@ public class Context {
}
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))));
foreach(Connection conn in conns)
await conn.Send(packet);
@ -373,18 +370,18 @@ public class Context {
await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name));
}
public async Task UpdateChannel(Channel channel, bool? temporary = null, int? hierarchy = null, string? password = null) {
if(!Channels.Any(c => c.NameEquals(channel.Name)))
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;
public async Task UpdateChannel(
Channel channel,
bool? temporary = null,
int? rank = null,
string? password = null
) {
Channels.UpdateChannel(
channel,
temporary: temporary,
rank: rank,
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(User user in Users.Where(u => u.Rank >= channel.Rank))
@ -392,20 +389,13 @@ public class Context {
}
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
Channels.Remove(channel);
Channels.RemoveChannel(channel.Name);
// 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(User user in GetChannelUsers(channel))
await SwitchChannel(user, defaultChannel, string.Empty);
await SwitchChannel(user, Channels.DefaultChannel, string.Empty);
// Broadcast deletion of channel
foreach(User user in Users.Where(u => u.Rank >= channel.Rank))

View file

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

View file

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