diff --git a/SharpChat/ChatChannel.cs b/SharpChat/ChatChannel.cs index 06d3a6a..9632c09 100644 --- a/SharpChat/ChatChannel.cs +++ b/SharpChat/ChatChannel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Text; @@ -11,8 +10,6 @@ namespace SharpChat { public int Rank { get; set; } = 0; public ChatUser Owner { get; set; } = null; - private List Users { get; } = new(); - public bool HasPassword => !string.IsNullOrWhiteSpace(Password); @@ -22,37 +19,6 @@ namespace SharpChat { Name = name; } - public bool HasUser(ChatUser user) { - return Users.Contains(user); - } - - public void UserJoin(ChatUser user) { - if(!user.InChannel(this)) { - // Remove this, a different means for this should be established for V1 compat. - user.Channel?.UserLeave(user); - user.JoinChannel(this); - } - - if(!HasUser(user)) - Users.Add(user); - } - - public void UserLeave(ChatUser user) { - Users.Remove(user); - - if(user.InChannel(this)) - user.LeaveChannel(this); - } - - public IEnumerable GetUsers(IEnumerable exclude = null) { - IEnumerable users = Users.OrderByDescending(x => x.Rank); - - if(exclude != null) - users = users.Except(exclude); - - return users.ToList(); - } - public string Pack() { StringBuilder sb = new(); diff --git a/SharpChat/ChatContext.cs b/SharpChat/ChatContext.cs index abadd5a..54d16ed 100644 --- a/SharpChat/ChatContext.cs +++ b/SharpChat/ChatContext.cs @@ -9,6 +9,8 @@ using System.Net; namespace SharpChat { public class ChatContext { + public record ChannelUserAssoc(long UserId, string ChannelName); + public HashSet Channels { get; } = new(); public readonly object ChannelsAccess = new(); @@ -21,6 +23,9 @@ namespace SharpChat { public IEventStorage Events { get; } public readonly object EventsAccess = new(); + public HashSet ChannelUsers { get; } = new(); + public readonly object ChannelUsersAccess = new(); + public ChatContext(IEventStorage evtStore) { Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); } @@ -38,7 +43,7 @@ namespace SharpChat { lock(UsersAccess) foreach(ChatUser user in Users) if(!Connections.Any(conn => conn.User == user)) { - UserLeave(null, user, UserDisconnectReason.TimeOut); + HandleDisconnect(user, UserDisconnectReason.TimeOut); Logger.Write($"Timed out {user} (no more connections)."); } } @@ -48,6 +53,42 @@ namespace SharpChat { return Connections.FirstOrDefault(s => s.Socket == sock); } + public bool IsInChannel(ChatUser user, ChatChannel channel) { + lock(ChannelUsersAccess) + return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name)); + } + + public string[] GetUserChannelNames(ChatUser user) { + lock(ChannelUsersAccess) + return ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName).ToArray(); + } + + public ChatChannel[] GetUserChannels(ChatUser user) { + string[] names = GetUserChannelNames(user); + lock(ChannelsAccess) + return Channels.Where(c => names.Any(n => c.NameEquals(n))).ToArray(); + } + + public long[] GetChannelUserIds(ChatChannel channel) { + lock(ChannelUsersAccess) + return ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId).ToArray(); + } + + public ChatUser[] GetChannelUsers(ChatChannel channel) { + long[] ids = GetChannelUserIds(channel); + lock(UsersAccess) + return Users.Where(u => ids.Contains(u.UserId)).ToArray(); + } + + public void DebugPrintChannelUsers() { + lock(ChannelUsersAccess) { + Logger.Write("DebugPrintChannelUsers()"); + foreach(ChannelUserAssoc cua in ChannelUsers) + Logger.Write(cua); + Logger.Write(string.Empty); + } + } + public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) { if(duration > TimeSpan.Zero) SendTo(user, new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration)); @@ -61,18 +102,18 @@ namespace SharpChat { Connections.RemoveWhere(conn => conn.IsDisposed); } - UserLeave(user.Channel, user, reason); + HandleDisconnect(user, reason); } public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) { lock(EventsAccess) { - if(!chan.HasUser(user)) { + if(!IsInChannel(user, chan)) { SendTo(chan, new UserConnectPacket(DateTimeOffset.Now, user)); Events.AddEvent(new UserConnectEvent(DateTimeOffset.Now, user, chan)); } conn.Send(new AuthSuccessPacket(user, chan, conn, maxMsgLength)); - conn.Send(new ContextUsersPacket(chan.GetUsers(new[] { user }))); + conn.Send(new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank))); foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name)) conn.Send(new ContextMessagePacket(msg)); @@ -80,32 +121,38 @@ namespace SharpChat { lock(ChannelsAccess) conn.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank))); - if(!chan.HasUser(user)) - chan.UserJoin(user); - lock(UsersAccess) Users.Add(user); + + lock(ChannelUsersAccess) { + ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); + user.CurrentChannel = chan; + } } } - public void UserLeave(ChatChannel chan, ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) { + public void HandleDisconnect(ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) { user.Status = ChatUserStatus.Offline; - if(chan == null) { - foreach(ChatChannel channel in user.GetChannels()) { - UserLeave(channel, user, reason); - } - return; - } - - if(chan.IsTemporary && chan.Owner == user) - lock(ChannelsAccess) - RemoveChannel(chan); - lock(EventsAccess) { - chan.UserLeave(user); - SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); - Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); + lock(UsersAccess) + Users.Remove(user); + + lock(ChannelUsersAccess) { + ChatChannel[] channels = GetUserChannels(user); + + foreach(ChatChannel chan in channels) { + ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name)); + DebugPrintChannelUsers(); + + SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); + Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); + + if(chan.IsTemporary && chan.Owner == user) + lock(ChannelsAccess) + RemoveChannel(chan); + } + } } } @@ -146,14 +193,18 @@ namespace SharpChat { Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan)); SendTo(user, new ContextClearPacket(chan, ContextClearMode.MessagesUsers)); - SendTo(user, new ContextUsersPacket(chan.GetUsers(new[] { user }))); + SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank))); foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name)) SendTo(user, new ContextMessagePacket(msg)); ForceChannel(user, chan); - oldChan.UserLeave(user); - chan.UserJoin(user); + + lock(ChannelUsersAccess) { + ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name)); + ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); + user.CurrentChannel = chan; + } } if(oldChan.IsTemporary && oldChan.Owner == user) @@ -189,8 +240,9 @@ namespace SharpChat { if(packet == null) throw new ArgumentNullException(nameof(packet)); + // might be faster to grab the users first and then cascade into that SendTo lock(ConnectionsAccess) { - IEnumerable conns = Connections.Where(c => c.IsAuthed && channel.HasUser(c.User)); + IEnumerable conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel)); foreach(ChatConnection conn in conns) conn.Send(packet); } @@ -257,7 +309,7 @@ namespace SharpChat { // Move all users back to the main channel // TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel. - foreach(ChatUser user in channel.GetUsers()) + foreach(ChatUser user in GetChannelUsers(channel)) SwitchChannel(user, defaultChannel, string.Empty); // Broadcast deletion of channel diff --git a/SharpChat/ChatUser.cs b/SharpChat/ChatUser.cs index 97c9240..813b878 100644 --- a/SharpChat/ChatUser.cs +++ b/SharpChat/ChatUser.cs @@ -1,7 +1,5 @@ using SharpChat.Misuzu; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; namespace SharpChat { @@ -81,14 +79,10 @@ namespace SharpChat { public class ChatUser : BasicUser { public DateTimeOffset SilencedUntil { get; set; } - private readonly List Channels = new(); - public readonly ChatRateLimiter RateLimiter = new(); - public ChatChannel Channel => Channels.FirstOrDefault(); - // This needs to be a session thing - public ChatChannel CurrentChannel { get; private set; } + public ChatChannel CurrentChannel { get; set; } public bool IsSilenced => DateTimeOffset.UtcNow - SilencedUntil <= TimeSpan.Zero; @@ -114,26 +108,6 @@ namespace SharpChat { SilencedUntil = auth.SilencedUntil; } - public bool InChannel(ChatChannel chan) { - return Channels.Contains(chan); - } - - public void JoinChannel(ChatChannel chan) { - if(!InChannel(chan)) { - Channels.Add(chan); - CurrentChannel = chan; - } - } - - public void LeaveChannel(ChatChannel chan) { - Channels.Remove(chan); - CurrentChannel = Channels.FirstOrDefault(); - } - - public IEnumerable GetChannels() { - return Channels.ToList(); - } - public bool NameEquals(string name) { return string.Equals(name, Username, StringComparison.InvariantCultureIgnoreCase) || string.Equals(name, Nickname, StringComparison.InvariantCultureIgnoreCase) diff --git a/SharpChat/Commands/WhoCommand.cs b/SharpChat/Commands/WhoCommand.cs index 950a31a..87c843d 100644 --- a/SharpChat/Commands/WhoCommand.cs +++ b/SharpChat/Commands/WhoCommand.cs @@ -44,7 +44,7 @@ namespace SharpChat.Commands { return; } - foreach(ChatUser whoUser in whoChan.GetUsers()) { + foreach(ChatUser whoUser in ctx.Chat.GetChannelUsers(whoChan)) { whoChanSB.Append(@" c.User == conn.User)) - Context.UserLeave(null, conn.User); + Context.HandleDisconnect(conn.User); } Context.Update();