using Fleck; using SharpChat.Events; using SharpChat.EventStorage; using SharpChat.Packet; using System; using System.Collections.Generic; using System.Linq; namespace SharpChat { public class ChatContext { public HashSet Channels { get; } = new(); public readonly object ChannelsAccess = new(); public HashSet Connections { get; } = new(); public readonly object ConnectionsAccess = new(); public HashSet Users { get; } = new(); public readonly object UsersAccess = new(); public IEventStorage Events { get; } public readonly object EventsAccess = new(); public ChatContext(IEventStorage evtStore) { Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); } public void Update() { lock(UsersAccess) foreach(ChatUser user in Users) { IEnumerable timedOut = user.GetDeadConnections(); foreach(ChatConnection conn in timedOut) { user.RemoveConnection(conn); conn.Dispose(); Logger.Write($"Nuked session {conn.Id} from {user.Username} (timeout)"); } if(!user.HasConnections) UserLeave(null, user, UserDisconnectReason.TimeOut); } } public ChatConnection GetConnection(IWebSocketConnection sock) { return Connections.FirstOrDefault(s => s.Socket == sock); } public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) { if(duration > TimeSpan.Zero) user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration)); else user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Kicked)); user.Close(); UserLeave(user.Channel, user, reason); } public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) { lock(EventsAccess) { if(!chan.HasUser(user)) { chan.Send(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 }))); foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name)) conn.Send(new ContextMessagePacket(msg)); 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); } } public void UserLeave(ChatChannel chan, 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); chan.Send(new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); } } public void SwitchChannel(ChatUser user, ChatChannel chan, string password) { if(user.CurrentChannel == chan) { //user.Send(true, "samechan", chan.Name); user.ForceChannel(); return; } if(!user.Can(ChatUserPermissions.JoinAnyChannel) && chan.Owner != user) { if(chan.Rank > user.Rank) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name)); user.ForceChannel(); return; } if(chan.Password != password) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name)); user.ForceChannel(); return; } } ForceChannelSwitch(user, chan); } public void ForceChannelSwitch(ChatUser user, ChatChannel chan) { lock(ChannelsAccess) if(!Channels.Contains(chan)) return; ChatChannel oldChan = user.CurrentChannel; lock(EventsAccess) { oldChan.Send(new UserChannelLeavePacket(user)); Events.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan)); chan.Send(new UserChannelJoinPacket(user)); Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan)); user.Send(new ContextClearPacket(chan, ContextClearMode.MessagesUsers)); user.Send(new ContextUsersPacket(chan.GetUsers(new[] { user }))); foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name)) user.Send(new ContextMessagePacket(msg)); user.ForceChannel(chan); oldChan.UserLeave(user); chan.UserJoin(user); } if(oldChan.IsTemporary && oldChan.Owner == user) lock(ChannelsAccess) RemoveChannel(oldChan); } public void Send(IServerPacket packet) { lock(UsersAccess) foreach(ChatUser user in Users) user.Send(packet); } public void UpdateChannel(ChatChannel channel, string name = null, bool? temporary = null, int? hierarchy = null, string password = null) { if(channel == null) throw new ArgumentNullException(nameof(channel)); if(!Channels.Contains(channel)) throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel)); string prevName = channel.Name; int prevHierarchy = channel.Rank; bool nameUpdated = !string.IsNullOrWhiteSpace(name) && name != prevName; if(nameUpdated) { if(!ChatChannel.CheckName(name)) throw new ArgumentException("Name contains invalid characters.", nameof(name)); channel.Name = name; } if(temporary.HasValue) channel.IsTemporary = temporary.Value; if(hierarchy.HasValue) channel.Rank = hierarchy.Value; if(password != null) channel.Password = password; // 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 lock(UsersAccess) foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) { user.Send(new ChannelUpdatePacket(prevName, channel)); if(nameUpdated) user.ForceChannel(); } } public void RemoveChannel(ChatChannel channel) { if(channel == null || !Channels.Any()) return; ChatChannel defaultChannel = Channels.FirstOrDefault(); if(defaultChannel == null) return; // Remove channel from the listing Channels.Remove(channel); // Move all users back to the main channel // TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel. foreach(ChatUser user in channel.GetUsers()) SwitchChannel(user, defaultChannel, string.Empty); // Broadcast deletion of channel lock(UsersAccess) foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) user.Send(new ChannelDeletePacket(channel)); } } }