diff --git a/SharpChat/ChannelManager.cs b/SharpChat/ChannelManager.cs deleted file mode 100644 index 8911563..0000000 --- a/SharpChat/ChannelManager.cs +++ /dev/null @@ -1,160 +0,0 @@ -using SharpChat.Packet; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SharpChat { - public class ChannelException : Exception { } - public class ChannelExistException : ChannelException { } - public class ChannelInvalidNameException : ChannelException { } - - public class ChannelManager : IDisposable { - private readonly List Channels = new(); - - public readonly ChatContext Context; - - public bool IsDisposed { get; private set; } - - public ChannelManager(ChatContext context) { - Context = context; - } - - private ChatChannel _DefaultChannel; - - public ChatChannel DefaultChannel { - get { - if(_DefaultChannel == null) - _DefaultChannel = Channels.FirstOrDefault(); - - return _DefaultChannel; - } - set { - if(value == null) - return; - - if(Channels.Contains(value)) - _DefaultChannel = value; - } - } - - - public void Add(ChatChannel channel) { - if(channel == null) - throw new ArgumentNullException(nameof(channel)); - if(!channel.Name.All(c => char.IsLetter(c) || char.IsNumber(c) || c == '-')) - throw new ChannelInvalidNameException(); - if(Get(channel.Name) != null) - throw new ChannelExistException(); - - // Add channel to the listing - Channels.Add(channel); - - // Set as default if there's none yet - if(_DefaultChannel == null) - _DefaultChannel = channel; - - // Broadcast creation of channel - foreach(ChatUser user in Context.Users.OfHierarchy(channel.Rank)) - user.Send(new ChannelCreatePacket(channel)); - } - - public void Remove(ChatChannel channel) { - if(channel == null || channel == DefaultChannel) - 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()) { - Context.SwitchChannel(user, DefaultChannel, string.Empty); - } - - // Broadcast deletion of channel - foreach(ChatUser user in Context.Users.OfHierarchy(channel.Rank)) - user.Send(new ChannelDeletePacket(channel)); - } - - public bool Contains(ChatChannel chan) { - if(chan == null) - return false; - - lock(Channels) - return Channels.Contains(chan) || Channels.Any(c => c.Name.ToLowerInvariant() == chan.Name.ToLowerInvariant()); - } - - public void Update(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(!name.All(c => char.IsLetter(c) || char.IsNumber(c) || c == '-')) - throw new ChannelInvalidNameException(); - if(Get(name) != null) - throw new ChannelExistException(); - - 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 - foreach(ChatUser user in Context.Users.OfHierarchy(channel.Rank)) { - user.Send(new ChannelUpdatePacket(prevName, channel)); - - if(nameUpdated) - user.ForceChannel(); - } - } - - public ChatChannel Get(string name) { - if(string.IsNullOrWhiteSpace(name)) - return null; - - return Channels.FirstOrDefault(x => x.Name.ToLowerInvariant() == name.ToLowerInvariant()); - } - - public IEnumerable GetUser(ChatUser user) { - if(user == null) - return null; - - return Channels.Where(x => x.HasUser(user)); - } - - public IEnumerable OfHierarchy(int hierarchy) { - lock(Channels) - return Channels.Where(c => c.Rank <= hierarchy).ToList(); - } - - ~ChannelManager() { - DoDispose(); - } - - public void Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { - if(IsDisposed) - return; - IsDisposed = true; - - Channels.Clear(); - } - } -} diff --git a/SharpChat/ChatChannel.cs b/SharpChat/ChatChannel.cs index 34b7840..c239497 100644 --- a/SharpChat/ChatChannel.cs +++ b/SharpChat/ChatChannel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; @@ -24,8 +25,7 @@ namespace SharpChat { } public bool HasUser(ChatUser user) { - lock(Users) - return Users.Contains(user); + return Users.Contains(user); } public void UserJoin(ChatUser user) { @@ -35,36 +35,29 @@ namespace SharpChat { user.JoinChannel(this); } - lock(Users) { - if(!HasUser(user)) - Users.Add(user); - } + if(!HasUser(user)) + Users.Add(user); } public void UserLeave(ChatUser user) { - lock(Users) - Users.Remove(user); + Users.Remove(user); if(user.InChannel(this)) user.LeaveChannel(this); } public void Send(IServerPacket packet) { - lock(Users) { - foreach(ChatUser user in Users) - user.Send(packet); - } + foreach(ChatUser user in Users) + user.Send(packet); } public IEnumerable GetUsers(IEnumerable exclude = null) { - lock(Users) { - IEnumerable users = Users.OrderByDescending(x => x.Rank); + IEnumerable users = Users.OrderByDescending(x => x.Rank); - if(exclude != null) - users = users.Except(exclude); + if(exclude != null) + users = users.Except(exclude); - return users.ToList(); - } + return users.ToList(); } public string Pack() { @@ -78,5 +71,21 @@ namespace SharpChat { return sb.ToString(); } + + public bool NameEquals(string name) { + return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase); + } + + public override int GetHashCode() { + return Name.GetHashCode(); + } + + public static bool CheckName(string name) { + return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar); + } + + public static bool CheckNameChar(char c) { + return char.IsLetter(c) || char.IsNumber(c) || c == '-'; + } } } diff --git a/SharpChat/ChatContext.cs b/SharpChat/ChatContext.cs index 6bb4d0f..3f60ef6 100644 --- a/SharpChat/ChatContext.cs +++ b/SharpChat/ChatContext.cs @@ -1,24 +1,39 @@ using SharpChat.Events; +using SharpChat.EventStorage; using SharpChat.Packet; using System; using System.Collections.Generic; +using System.Linq; namespace SharpChat { - public class ChatContext : IDisposable { - public bool IsDisposed { get; private set; } + public class ChatContext { + public HashSet Channels { get; } = new(); + public readonly object ChannelsAccess = new(); - public ChannelManager Channels { get; } - public UserManager Users { get; } - public ChatEventManager Events { get; } + public HashSet Users { get; } = new(); + public readonly object UsersAccess = new(); - public ChatContext() { - Users = new(this); - Channels = new(this); - Events = new(this); + public IEventStorage Events { get; } + public readonly object EventsAccess = new(); + + public ChatContext(IEventStorage evtStore) { + Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); } public void Update() { - CheckPings(); + lock(UsersAccess) + foreach(ChatUser user in Users) { + IEnumerable timedOut = user.GetDeadSessions(); + + foreach(ChatUserSession sess in timedOut) { + user.RemoveSession(sess); + sess.Dispose(); + Logger.Write($"Nuked session {sess.Id} from {user.Username} (timeout)"); + } + + if(!user.HasSessions) + UserLeave(null, user, UserDisconnectReason.TimeOut); + } } public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) { @@ -32,26 +47,27 @@ namespace SharpChat { } public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess, int maxMsgLength) { - if(!chan.HasUser(user)) { - chan.Send(new UserConnectPacket(DateTimeOffset.Now, user)); - Events.Add(new UserConnectEvent(DateTimeOffset.Now, user, chan)); + lock(EventsAccess) { + if(!chan.HasUser(user)) { + chan.Send(new UserConnectPacket(DateTimeOffset.Now, user)); + Events.AddEvent(new UserConnectEvent(DateTimeOffset.Now, user, chan)); + } + + sess.Send(new AuthSuccessPacket(user, chan, sess, maxMsgLength)); + sess.Send(new ContextUsersPacket(chan.GetUsers(new[] { user }))); + + foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name)) + sess.Send(new ContextMessagePacket(msg)); + + lock(ChannelsAccess) + sess.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank))); + + if(!chan.HasUser(user)) + chan.UserJoin(user); + + lock(UsersAccess) + Users.Add(user); } - - sess.Send(new AuthSuccessPacket(user, chan, sess, maxMsgLength)); - sess.Send(new ContextUsersPacket(chan.GetUsers(new[] { user }))); - - IEnumerable msgs = Events.GetTargetLog(chan); - - foreach(IChatEvent msg in msgs) - sess.Send(new ContextMessagePacket(msg)); - - sess.Send(new ContextChannelsPacket(Channels.OfHierarchy(user.Rank))); - - if(!chan.HasUser(user)) - chan.UserJoin(user); - - if(!Users.Contains(user)) - Users.Add(user); } public void UserLeave(ChatChannel chan, ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) { @@ -65,11 +81,14 @@ namespace SharpChat { } if(chan.IsTemporary && chan.Owner == user) - Channels.Remove(chan); + lock(ChannelsAccess) + RemoveChannel(chan); - chan.UserLeave(user); - chan.Send(new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); - Events.Add(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); + 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) { @@ -97,70 +116,96 @@ namespace SharpChat { } public void ForceChannelSwitch(ChatUser user, ChatChannel chan) { - if(!Channels.Contains(chan)) - return; + lock(ChannelsAccess) + if(!Channels.Contains(chan)) + return; ChatChannel oldChan = user.CurrentChannel; - oldChan.Send(new UserChannelLeavePacket(user)); - Events.Add(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan)); - chan.Send(new UserChannelJoinPacket(user)); - Events.Add(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan)); + 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 }))); + user.Send(new ContextClearPacket(chan, ContextClearMode.MessagesUsers)); + user.Send(new ContextUsersPacket(chan.GetUsers(new[] { user }))); - IEnumerable msgs = Events.GetTargetLog(chan); + foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name)) + user.Send(new ContextMessagePacket(msg)); - foreach(IChatEvent msg in msgs) - user.Send(new ContextMessagePacket(msg)); - - user.ForceChannel(chan); - oldChan.UserLeave(user); - chan.UserJoin(user); + user.ForceChannel(chan); + oldChan.UserLeave(user); + chan.UserJoin(user); + } if(oldChan.IsTemporary && oldChan.Owner == user) - Channels.Remove(oldChan); - } - - public void CheckPings() { - lock(Users) - foreach(ChatUser user in Users.All()) { - IEnumerable timedOut = user.GetDeadSessions(); - - foreach(ChatUserSession sess in timedOut) { - user.RemoveSession(sess); - sess.Dispose(); - Logger.Write($"Nuked session {sess.Id} from {user.Username} (timeout)"); - } - - if(!user.HasSessions) - UserLeave(null, user, UserDisconnectReason.TimeOut); - } + lock(ChannelsAccess) + RemoveChannel(oldChan); } public void Send(IServerPacket packet) { - foreach(ChatUser user in Users.All()) - user.Send(packet); + lock(UsersAccess) + foreach(ChatUser user in Users) + user.Send(packet); } - ~ChatContext() { - DoDispose(); + 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 Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { - if(IsDisposed) + public void RemoveChannel(ChatChannel channel) { + if(channel == null || !Channels.Any()) return; - IsDisposed = true; - Events?.Dispose(); - Channels?.Dispose(); - Users?.Dispose(); + 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)); } } } diff --git a/SharpChat/ChatEventManager.cs b/SharpChat/ChatEventManager.cs deleted file mode 100644 index 1fcdd4d..0000000 --- a/SharpChat/ChatEventManager.cs +++ /dev/null @@ -1,100 +0,0 @@ -using SharpChat.Events; -using SharpChat.Packet; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SharpChat { - public class ChatEventManager : IDisposable { - private readonly List Events = null; - - public readonly ChatContext Context; - - public bool IsDisposed { get; private set; } - - public ChatEventManager(ChatContext context) { - Context = context; - - if(!Database.HasDatabase) - Events = new(); - } - - public void Add(IChatEvent evt) { - if(evt == null) - throw new ArgumentNullException(nameof(evt)); - - if(Events != null) - lock(Events) - Events.Add(evt); - - if(Database.HasDatabase) - Database.LogEvent(evt); - } - - public void Remove(IChatEvent evt) { - if(evt == null) - return; - - if(Events != null) - lock(Events) - Events.Remove(evt); - - if(Database.HasDatabase) - Database.DeleteEvent(evt); - - Context.Send(new ChatMessageDeletePacket(evt.SequenceId)); - } - - public IChatEvent Get(long seqId) { - if(seqId < 1) - return null; - - if(Database.HasDatabase) - return Database.GetEvent(seqId); - - if(Events != null) - lock(Events) - return Events.FirstOrDefault(e => e.SequenceId == seqId); - - return null; - } - - public IEnumerable GetTargetLog(IPacketTarget target, int amount = 20, int offset = 0) { - if(Database.HasDatabase) - return Database.GetEvents(target, amount, offset).Reverse(); - - if(Events != null) - lock(Events) { - IEnumerable subset = Events.Where(e => e.Target == target || e.Target == null); - - int start = subset.Count() - offset - amount; - - if(start < 0) { - amount += start; - start = 0; - } - - return subset.Skip(start).Take(amount).ToList(); - } - - return Enumerable.Empty(); - } - - ~ChatEventManager() { - DoDispose(); - } - - public void Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { - if(IsDisposed) - return; - IsDisposed = true; - - Events?.Clear(); - } - } -} diff --git a/SharpChat/ChatRateLimiter.cs b/SharpChat/ChatRateLimiter.cs index 0e5b988..2a3af13 100644 --- a/SharpChat/ChatRateLimiter.cs +++ b/SharpChat/ChatRateLimiter.cs @@ -17,17 +17,15 @@ namespace SharpChat { public ChatRateLimitState State { get { - lock(TimePoints) { - if(TimePoints.Count == FLOOD_PROTECTION_AMOUNT) { - if((TimePoints.Last() - TimePoints.First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD) - return ChatRateLimitState.Kick; + if(TimePoints.Count == FLOOD_PROTECTION_AMOUNT) { + if((TimePoints.Last() - TimePoints.First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD) + return ChatRateLimitState.Kick; - if((TimePoints.Last() - TimePoints.Skip(5).First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD) - return ChatRateLimitState.Warning; - } - - return ChatRateLimitState.None; + if((TimePoints.Last() - TimePoints.Skip(5).First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD) + return ChatRateLimitState.Warning; } + + return ChatRateLimitState.None; } } @@ -35,12 +33,10 @@ namespace SharpChat { if(!dto.HasValue) dto = DateTimeOffset.Now; - lock(TimePoints) { - if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT) - TimePoints.Dequeue(); + if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT) + TimePoints.Dequeue(); - TimePoints.Enqueue(dto.Value); - } + TimePoints.Enqueue(dto.Value); } } } diff --git a/SharpChat/ChatUser.cs b/SharpChat/ChatUser.cs index 7a97c01..677e49d 100644 --- a/SharpChat/ChatUser.cs +++ b/SharpChat/ChatUser.cs @@ -2,7 +2,6 @@ using SharpChat.Packet; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Text; @@ -23,10 +22,6 @@ namespace SharpChat { public bool HasFloodProtection => Rank < RANK_NO_FLOOD; - public bool Equals([AllowNull] BasicUser other) { - return UserId == other.UserId; - } - public string DisplayName { get { StringBuilder sb = new(); @@ -71,6 +66,18 @@ namespace SharpChat { return sb.ToString(); } + + public override int GetHashCode() { + return UserId.GetHashCode(); + } + + public override bool Equals(object obj) { + return Equals(obj as BasicUser); + } + + public bool Equals(BasicUser other) { + return UserId == other?.UserId; + } } public class ChatUser : BasicUser, IPacketTarget { @@ -83,12 +90,7 @@ namespace SharpChat { public string TargetName => "@log"; - public ChatChannel Channel { - get { - lock(Channels) - return Channels.FirstOrDefault(); - } - } + public ChatChannel Channel => Channels.FirstOrDefault(); // This needs to be a session thing public ChatChannel CurrentChannel { get; private set; } @@ -96,26 +98,11 @@ namespace SharpChat { public bool IsSilenced => DateTimeOffset.UtcNow - SilencedUntil <= TimeSpan.Zero; - public bool HasSessions { - get { - lock(Sessions) - return Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Any(); - } - } + public bool HasSessions => Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Any(); - public int SessionCount { - get { - lock(Sessions) - return Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Count(); - } - } + public int SessionCount => Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Count(); - public IEnumerable RemoteAddresses { - get { - lock(Sessions) - return Sessions.Select(c => c.RemoteAddress); - } - } + public IEnumerable RemoteAddresses => Sessions.Select(c => c.RemoteAddress); public ChatUser() { } @@ -140,17 +127,14 @@ namespace SharpChat { } public void Send(IServerPacket packet) { - lock(Sessions) - foreach(ChatUserSession conn in Sessions) - conn.Send(packet); + foreach(ChatUserSession conn in Sessions) + conn.Send(packet); } public void Close() { - lock(Sessions) { - foreach(ChatUserSession conn in Sessions) - conn.Dispose(); - Sessions.Clear(); - } + foreach(ChatUserSession conn in Sessions) + conn.Dispose(); + Sessions.Clear(); } public void ForceChannel(ChatChannel chan = null) { @@ -158,36 +142,28 @@ namespace SharpChat { } public void FocusChannel(ChatChannel chan) { - lock(Channels) { - if(InChannel(chan)) - CurrentChannel = chan; - } + if(InChannel(chan)) + CurrentChannel = chan; } public bool InChannel(ChatChannel chan) { - lock(Channels) - return Channels.Contains(chan); + return Channels.Contains(chan); } public void JoinChannel(ChatChannel chan) { - lock(Channels) { - if(!InChannel(chan)) { - Channels.Add(chan); - CurrentChannel = chan; - } + if(!InChannel(chan)) { + Channels.Add(chan); + CurrentChannel = chan; } } public void LeaveChannel(ChatChannel chan) { - lock(Channels) { - Channels.Remove(chan); - CurrentChannel = Channels.FirstOrDefault(); - } + Channels.Remove(chan); + CurrentChannel = Channels.FirstOrDefault(); } public IEnumerable GetChannels() { - lock(Channels) - return Channels.ToList(); + return Channels.ToList(); } public void AddSession(ChatUserSession sess) { @@ -195,8 +171,7 @@ namespace SharpChat { return; sess.User = this; - lock(Sessions) - Sessions.Add(sess); + Sessions.Add(sess); } public void RemoveSession(ChatUserSession sess) { @@ -205,13 +180,17 @@ namespace SharpChat { if(!sess.IsDisposed) // this could be possible sess.User = null; - lock(Sessions) - Sessions.Remove(sess); + Sessions.Remove(sess); } public IEnumerable GetDeadSessions() { - lock(Sessions) - return Sessions.Where(x => x.HasTimedOut || x.IsDisposed).ToList(); + return Sessions.Where(x => x.HasTimedOut || x.IsDisposed).ToList(); + } + + public bool NameEquals(string name) { + return string.Equals(name, Username, StringComparison.InvariantCultureIgnoreCase) + || string.Equals(name, Nickname, StringComparison.InvariantCultureIgnoreCase) + || string.Equals(name, DisplayName, StringComparison.InvariantCultureIgnoreCase); } } } diff --git a/SharpChat/Config/CachedValue.cs b/SharpChat/Config/CachedValue.cs index 157415b..828c4a6 100644 --- a/SharpChat/Config/CachedValue.cs +++ b/SharpChat/Config/CachedValue.cs @@ -6,14 +6,14 @@ namespace SharpChat.Config { private string Name { get; } private TimeSpan Lifetime { get; } private T Fallback { get; } - private object Sync { get; } = new(); + private object ConfigAccess { get; } = new(); private object CurrentValue { get; set; } private DateTimeOffset LastRead { get; set; } public T Value { get { - lock(Sync) { + 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) { LastRead = now; @@ -37,9 +37,7 @@ namespace SharpChat.Config { } public void Refresh() { - lock(Sync) { - LastRead = DateTimeOffset.MinValue; - } + LastRead = DateTimeOffset.MinValue; } public override string ToString() { diff --git a/SharpChat/EventStorage/IEventStorage.cs b/SharpChat/EventStorage/IEventStorage.cs new file mode 100644 index 0000000..62ea469 --- /dev/null +++ b/SharpChat/EventStorage/IEventStorage.cs @@ -0,0 +1,12 @@ +using SharpChat.Events; +using System; +using System.Collections.Generic; + +namespace SharpChat.EventStorage { + public interface IEventStorage { + void AddEvent(IChatEvent evt); + void RemoveEvent(IChatEvent evt); + IChatEvent GetEvent(long seqId); + IEnumerable GetTargetEventLog(string target, int amount = 20, int offset = 0); + } +} diff --git a/SharpChat/Database.cs b/SharpChat/EventStorage/MariaDBEventStorage.cs similarity index 56% rename from SharpChat/Database.cs rename to SharpChat/EventStorage/MariaDBEventStorage.cs index 178d8c0..2f211ae 100644 --- a/SharpChat/Database.cs +++ b/SharpChat/EventStorage/MariaDBEventStorage.cs @@ -1,113 +1,22 @@ using MySqlConnector; -using SharpChat.Config; using SharpChat.Events; using System; using System.Collections.Generic; using System.Text; using System.Text.Json; -namespace SharpChat { - public static partial class Database { - private static string ConnectionString = null; +namespace SharpChat.EventStorage { + public partial class MariaDBEventStorage : IEventStorage { + private string ConnectionString { get; } - public static bool HasDatabase - => !string.IsNullOrWhiteSpace(ConnectionString); - - public static void Init(IConfig config) { - Init( - config.ReadValue("host", "localhost"), - config.ReadValue("user", string.Empty), - config.ReadValue("pass", string.Empty), - config.ReadValue("db", "sharpchat") - ); + public MariaDBEventStorage(string connString) { + ConnectionString = connString ?? throw new ArgumentNullException(nameof(connString)); } - public static void Init(string host, string username, string password, string database) { - ConnectionString = new MySqlConnectionStringBuilder { - Server = host, - UserID = username, - Password = password, - Database = database, - OldGuids = false, - TreatTinyAsBoolean = false, - CharacterSet = "utf8mb4", - SslMode = MySqlSslMode.None, - ForceSynchronous = true, - ConnectionTimeout = 5, - }.ToString(); - RunMigrations(); - } + public void AddEvent(IChatEvent evt) { + if(evt == null) + throw new ArgumentNullException(nameof(evt)); - public static void Deinit() { - ConnectionString = null; - } - - private static MySqlConnection GetConnection() { - if(!HasDatabase) - return null; - - MySqlConnection conn = new(ConnectionString); - conn.Open(); - - return conn; - } - - private static int RunCommand(string command, params MySqlParameter[] parameters) { - if(!HasDatabase) - return 0; - - try { - using MySqlConnection conn = GetConnection(); - using MySqlCommand cmd = conn.CreateCommand(); - if(parameters?.Length > 0) - cmd.Parameters.AddRange(parameters); - cmd.CommandText = command; - return cmd.ExecuteNonQuery(); - } catch(MySqlException ex) { - Logger.Write(ex); - } - - return 0; - } - - private static MySqlDataReader RunQuery(string command, params MySqlParameter[] parameters) { - if(!HasDatabase) - return null; - - try { - MySqlConnection conn = GetConnection(); - MySqlCommand cmd = conn.CreateCommand(); - if(parameters?.Length > 0) - cmd.Parameters.AddRange(parameters); - cmd.CommandText = command; - return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection); - } catch(MySqlException ex) { - Logger.Write(ex); - } - - return null; - } - - private static object RunQueryValue(string command, params MySqlParameter[] parameters) { - if(!HasDatabase) - return null; - - try { - using MySqlConnection conn = GetConnection(); - using MySqlCommand cmd = conn.CreateCommand(); - if(parameters?.Length > 0) - cmd.Parameters.AddRange(parameters); - cmd.CommandText = command; - cmd.Prepare(); - return cmd.ExecuteScalar(); - } catch(MySqlException ex) { - Logger.Write(ex); - } - - return null; - } - - public static void LogEvent(IChatEvent evt) { if(evt.SequenceId < 1) evt.SequenceId = SharpId.Next(); @@ -131,67 +40,7 @@ namespace SharpChat { ); } - public static void DeleteEvent(IChatEvent evt) { - RunCommand( - "UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL", - new MySqlParameter("id", evt.SequenceId) - ); - } - - private static IChatEvent ReadEvent(MySqlDataReader reader, IPacketTarget target = null) { - Type evtType = Type.GetType(Encoding.ASCII.GetString((byte[])reader["event_type"])); - IChatEvent evt = JsonSerializer.Deserialize(Encoding.ASCII.GetString((byte[])reader["event_data"]), evtType) as IChatEvent; - evt.SequenceId = reader.GetInt64("event_id"); - evt.Target = target; - evt.TargetName = target?.TargetName ?? Encoding.ASCII.GetString((byte[])reader["event_target"]); - evt.Flags = (ChatMessageFlags)reader.GetByte("event_flags"); - evt.DateTime = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")); - - if(!reader.IsDBNull(reader.GetOrdinal("event_sender"))) { - evt.Sender = new BasicUser { - UserId = reader.GetInt64("event_sender"), - Username = reader.GetString("event_sender_name"), - Colour = ChatColour.FromMisuzu(reader.GetInt32("event_sender_colour")), - Rank = reader.GetInt32("event_sender_rank"), - Nickname = reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? null : reader.GetString("event_sender_nick"), - Permissions = (ChatUserPermissions)reader.GetInt32("event_sender_perms") - }; - } - - return evt; - } - - public static IEnumerable GetEvents(IPacketTarget target, int amount, int offset) { - List events = new(); - - try { - using MySqlDataReader reader = RunQuery( - "SELECT `event_id`, `event_type`, `event_flags`, `event_data`" - + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`" - + ", UNIX_TIMESTAMP(`event_created`) AS `event_created`" - + " FROM `sqc_events`" - + " WHERE `event_deleted` IS NULL AND `event_target` = @target" - + " AND `event_id` > @offset" - + " ORDER BY `event_id` DESC" - + " LIMIT @amount", - new MySqlParameter("target", target.TargetName), - new MySqlParameter("amount", amount), - new MySqlParameter("offset", offset) - ); - - while(reader.Read()) { - IChatEvent evt = ReadEvent(reader, target); - if(evt != null) - events.Add(evt); - } - } catch(MySqlException ex) { - Logger.Write(ex); - } - - return events; - } - - public static IChatEvent GetEvent(long seqId) { + public IChatEvent GetEvent(long seqId) { try { using MySqlDataReader reader = RunQuery( "SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`" @@ -213,5 +62,66 @@ namespace SharpChat { return null; } + + private static IChatEvent ReadEvent(MySqlDataReader reader) { + Type evtType = Type.GetType(Encoding.ASCII.GetString((byte[])reader["event_type"])); + IChatEvent evt = JsonSerializer.Deserialize(Encoding.ASCII.GetString((byte[])reader["event_data"]), evtType) as IChatEvent; + evt.SequenceId = reader.GetInt64("event_id"); + evt.TargetName = Encoding.ASCII.GetString((byte[])reader["event_target"]); + evt.Flags = (ChatMessageFlags)reader.GetByte("event_flags"); + evt.DateTime = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")); + + if(!reader.IsDBNull(reader.GetOrdinal("event_sender"))) { + evt.Sender = new BasicUser { + UserId = reader.GetInt64("event_sender"), + Username = reader.GetString("event_sender_name"), + Colour = ChatColour.FromMisuzu(reader.GetInt32("event_sender_colour")), + Rank = reader.GetInt32("event_sender_rank"), + Nickname = reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? null : reader.GetString("event_sender_nick"), + Permissions = (ChatUserPermissions)reader.GetInt32("event_sender_perms") + }; + } + + return evt; + } + + public IEnumerable GetTargetEventLog(string target, int amount = 20, int offset = 0) { + List events = new(); + + try { + using MySqlDataReader reader = RunQuery( + "SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`" + + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`" + + ", UNIX_TIMESTAMP(`event_created`) AS `event_created`" + + " FROM `sqc_events`" + + " WHERE `event_deleted` IS NULL AND `event_target` = @target" + + " AND `event_id` > @offset" + + " ORDER BY `event_id` DESC" + + " LIMIT @amount", + new MySqlParameter("target", target), + new MySqlParameter("amount", amount), + new MySqlParameter("offset", offset) + ); + + while(reader.Read()) { + IChatEvent evt = ReadEvent(reader); + if(evt != null) + events.Add(evt); + } + } catch(MySqlException ex) { + Logger.Write(ex); + } + + return events; + } + + public void RemoveEvent(IChatEvent evt) { + if(evt == null) + throw new ArgumentNullException(nameof(evt)); + RunCommand( + "UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL", + new MySqlParameter("id", evt.SequenceId) + ); + } } } diff --git a/SharpChat/EventStorage/MariaDBEventStorage_Database.cs b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs new file mode 100644 index 0000000..35ded05 --- /dev/null +++ b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs @@ -0,0 +1,82 @@ +using MySqlConnector; +using SharpChat.Config; + +namespace SharpChat.EventStorage { + public partial class MariaDBEventStorage { + public static string BuildConnString(IConfig config) { + return BuildConnString( + config.ReadValue("host", "localhost"), + config.ReadValue("user", string.Empty), + config.ReadValue("pass", string.Empty), + config.ReadValue("db", "sharpchat") + ); + } + + public static string BuildConnString(string host, string username, string password, string database) { + return new MySqlConnectionStringBuilder { + Server = host, + UserID = username, + Password = password, + Database = database, + OldGuids = false, + TreatTinyAsBoolean = false, + CharacterSet = "utf8mb4", + SslMode = MySqlSslMode.None, + ForceSynchronous = true, + ConnectionTimeout = 5, + }.ToString(); + } + + private MySqlConnection GetConnection() { + MySqlConnection conn = new(ConnectionString); + conn.Open(); + return conn; + } + + private int RunCommand(string command, params MySqlParameter[] parameters) { + try { + using MySqlConnection conn = GetConnection(); + using MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return cmd.ExecuteNonQuery(); + } catch(MySqlException ex) { + Logger.Write(ex); + } + + return 0; + } + + private MySqlDataReader RunQuery(string command, params MySqlParameter[] parameters) { + try { + MySqlConnection conn = GetConnection(); + MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection); + } catch(MySqlException ex) { + Logger.Write(ex); + } + + return null; + } + + private object RunQueryValue(string command, params MySqlParameter[] parameters) { + try { + using MySqlConnection conn = GetConnection(); + using MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + cmd.Prepare(); + return cmd.ExecuteScalar(); + } catch(MySqlException ex) { + Logger.Write(ex); + } + + return null; + } + } +} diff --git a/SharpChat/Database_Migrations.cs b/SharpChat/EventStorage/MariaDBEventStorage_Migrations.cs similarity index 91% rename from SharpChat/Database_Migrations.cs rename to SharpChat/EventStorage/MariaDBEventStorage_Migrations.cs index a92c0f2..8cd6f0e 100644 --- a/SharpChat/Database_Migrations.cs +++ b/SharpChat/EventStorage/MariaDBEventStorage_Migrations.cs @@ -1,9 +1,9 @@ using MySqlConnector; using System; -namespace SharpChat { - public static partial class Database { - private static void DoMigration(string name, Action action) { +namespace SharpChat.EventStorage { + public partial class MariaDBEventStorage { + private void DoMigration(string name, Action action) { bool done = (long)RunQueryValue( "SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name", new MySqlParameter("name", name) @@ -18,7 +18,7 @@ namespace SharpChat { } } - private static void RunMigrations() { + public void RunMigrations() { RunCommand( "CREATE TABLE IF NOT EXISTS `sqc_migrations` (" + "`migration_name` VARCHAR(255) NOT NULL," @@ -31,7 +31,7 @@ namespace SharpChat { DoMigration("create_events_table", CreateEventsTable); } - private static void CreateEventsTable() { + private void CreateEventsTable() { RunCommand( "CREATE TABLE `sqc_events` (" + "`event_id` BIGINT(20) NOT NULL," diff --git a/SharpChat/EventStorage/VirtualEventStorage.cs b/SharpChat/EventStorage/VirtualEventStorage.cs new file mode 100644 index 0000000..d2d8dd5 --- /dev/null +++ b/SharpChat/EventStorage/VirtualEventStorage.cs @@ -0,0 +1,38 @@ +using SharpChat.Events; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SharpChat.EventStorage { + public class VirtualEventStorage : IEventStorage { + private readonly Dictionary Events = new(); + + public void AddEvent(IChatEvent evt) { + if(evt == null) + throw new ArgumentNullException(nameof(evt)); + Events.Add(evt.SequenceId, evt); + } + + public IChatEvent GetEvent(long seqId) { + return Events.TryGetValue(seqId, out IChatEvent evt) ? evt : null; + } + + public void RemoveEvent(IChatEvent evt) { + if(evt == null) + throw new ArgumentNullException(nameof(evt)); + Events.Remove(evt.SequenceId); + } + + public IEnumerable GetTargetEventLog(string target, int amount = 20, int offset = 0) { + IEnumerable subset = Events.Values.Where(ev => ev.TargetName == target); + + int start = subset.Count() - offset - amount; + if(start < 0) { + amount += start; + start = 0; + } + + return subset.Skip(start).Take(amount).ToArray(); + } + } +} diff --git a/SharpChat/Program.cs b/SharpChat/Program.cs index 2a7dee4..e21eb75 100644 --- a/SharpChat/Program.cs +++ b/SharpChat/Program.cs @@ -1,4 +1,5 @@ using SharpChat.Config; +using SharpChat.EventStorage; using SharpChat.Misuzu; using System; using System.IO; @@ -46,8 +47,6 @@ namespace SharpChat { using IConfig config = new StreamConfig(configFile); - Database.Init(config.ScopeTo("mariadb")); - if(hasCancelled) return; using HttpClient httpClient = new(new HttpClientHandler() { @@ -61,7 +60,18 @@ namespace SharpChat { if(hasCancelled) return; - using SockChatServer scs = new(httpClient, msz, config.ScopeTo("chat")); + IEventStorage evtStore; + if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) { + evtStore = new VirtualEventStorage(); + } else { + MariaDBEventStorage mdbes = new(MariaDBEventStorage.BuildConnString(config.ScopeTo("mariadb"))); + evtStore = mdbes; + mdbes.RunMigrations(); + } + + if(hasCancelled) return; + + using SockChatServer scs = new(httpClient, msz, evtStore, config.ScopeTo("chat")); scs.Listen(mre); mre.WaitOne(); diff --git a/SharpChat/RNG.cs b/SharpChat/RNG.cs index 4c3d9c4..d5d0f11 100644 --- a/SharpChat/RNG.cs +++ b/SharpChat/RNG.cs @@ -3,28 +3,23 @@ using System.Security.Cryptography; namespace SharpChat { public static class RNG { - private static object Lock { get; } = new(); private static Random NormalRandom { get; } = new(); private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create(); public static int Next() { - lock(Lock) - return NormalRandom.Next(); + return NormalRandom.Next(); } public static int Next(int max) { - lock(Lock) - return NormalRandom.Next(max); + return NormalRandom.Next(max); } public static int Next(int min, int max) { - lock(Lock) - return NormalRandom.Next(min, max); + return NormalRandom.Next(min, max); } public static void NextBytes(byte[] buffer) { - lock(Lock) - SecureRandom.GetBytes(buffer); + SecureRandom.GetBytes(buffer); } } } diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index 969e6db..026e4f5 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -2,6 +2,7 @@ using SharpChat.Commands; using SharpChat.Config; using SharpChat.Events; +using SharpChat.EventStorage; using SharpChat.Misuzu; using SharpChat.Packet; using System; @@ -45,17 +46,19 @@ namespace SharpChat { }; public List Sessions { get; } = new List(); - private object SessionsLock { get; } = new object(); + private object SessionsAccess { get; } = new object(); public ChatUserSession GetSession(IWebSocketConnection conn) { - lock(SessionsLock) + lock(SessionsAccess) return Sessions.FirstOrDefault(x => x.Connection == conn); } private ManualResetEvent Shutdown { get; set; } private bool IsShuttingDown = false; - public SockChatServer(HttpClient httpClient, MisuzuClient msz, IConfig config) { + private ChatChannel DefaultChannel { get; set; } + + public SockChatServer(HttpClient httpClient, MisuzuClient msz, IEventStorage evtStore, IConfig config) { Logger.Write("Initialising Sock Chat server..."); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); @@ -65,7 +68,7 @@ namespace SharpChat { MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS); FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH); - Context = new ChatContext(); + Context = new ChatContext(evtStore); string[] channelNames = config.ReadValue("channels", new[] { "lounge" }); @@ -82,6 +85,8 @@ namespace SharpChat { channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0); Context.Channels.Add(channelInfo); + + DefaultChannel ??= channelInfo; } ushort port = config.SafeReadValue("port", DEFAULT_PORT); @@ -107,7 +112,9 @@ namespace SharpChat { } private void OnOpen(IWebSocketConnection conn) { - lock(SessionsLock) { + Logger.Write($"Connection opened from {conn.ConnectionInfo.ClientIpAddress}:{conn.ConnectionInfo.ClientPort}"); + + lock(SessionsAccess) { if(!Sessions.Any(x => x.Connection == conn)) Sessions.Add(new ChatUserSession(conn)); } @@ -116,6 +123,8 @@ namespace SharpChat { } private void OnClose(IWebSocketConnection conn) { + Logger.Write($"Connection closed from {conn.ConnectionInfo.ClientIpAddress}:{conn.ConnectionInfo.ClientPort}"); + ChatUserSession sess = GetSession(conn); // Remove connection from user @@ -133,7 +142,7 @@ namespace SharpChat { Context.Update(); // Remove connection from server - lock(SessionsLock) + lock(SessionsAccess) Sessions.Remove(sess); sess?.Dispose(); @@ -195,10 +204,12 @@ namespace SharpChat { lock(BumpAccess) { if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { - (string, string)[] bumpList = Context.Users - .Where(u => u.HasSessions && u.Status == ChatUserStatus.Online) - .Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty)) - .ToArray(); + (string, string)[] bumpList; + lock(Context.UsersAccess) + bumpList = Context.Users + .Where(u => u.HasSessions && u.Status == ChatUserStatus.Online) + .Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty)) + .ToArray(); if(bumpList.Any()) Task.Run(async () => { @@ -279,38 +290,40 @@ namespace SharpChat { return; } - ChatUser aUser = Context.Users.Get(fai.UserId); + lock(Context.UsersAccess) { + ChatUser aUser = Context.Users.FirstOrDefault(u => u.UserId == fai.UserId); - if(aUser == null) - aUser = new ChatUser(fai); - else { - aUser.ApplyAuth(fai); - aUser.Channel?.Send(new UserUpdatePacket(aUser)); + if(aUser == null) + aUser = new ChatUser(fai); + else { + aUser.ApplyAuth(fai); + aUser.Channel?.Send(new UserUpdatePacket(aUser)); + } + + // Enforce a maximum amount of connections per user + if(aUser.SessionCount >= MaxConnections) { + sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); + sess.Dispose(); + return; + } + + // Bumping the ping to prevent upgrading + sess.BumpPing(); + + aUser.AddSession(sess); + + sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, $"Welcome to Flashii Chat, {aUser.Username}!")); + + if(File.Exists("welcome.txt")) { + IEnumerable lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); + string line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); + + if(!string.IsNullOrWhiteSpace(line)) + sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); + } + + Context.HandleJoin(aUser, DefaultChannel, sess, MaxMessageLength); } - - // Enforce a maximum amount of connections per user - if(aUser.SessionCount >= MaxConnections) { - sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); - sess.Dispose(); - return; - } - - // Bumping the ping to prevent upgrading - sess.BumpPing(); - - aUser.AddSession(sess); - - sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, $"Welcome to Flashii Chat, {aUser.Username}!")); - - if(File.Exists("welcome.txt")) { - IEnumerable lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); - string line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); - - if(!string.IsNullOrWhiteSpace(line)) - sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); - } - - Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess, MaxMessageLength); }).Wait(); break; @@ -370,8 +383,10 @@ namespace SharpChat { Text = messageText, }; - Context.Events.Add(message); - mChannel.Send(new ChatMessageAddPacket(message)); + lock(Context.EventsAccess) { + Context.Events.AddEvent(message); + mChannel.Send(new ChatMessageAddPacket(message)); + } break; } } @@ -408,7 +423,8 @@ namespace SharpChat { int offset = 1; if(setOthersNick && parts.Length > 1 && long.TryParse(parts[1], out long targetUserId) && targetUserId > 0) { - targetUser = Context.Users.Get(targetUserId); + lock(Context.UsersAccess) + targetUser = Context.Users.FirstOrDefault(u => u.UserId == targetUserId); offset = 2; } @@ -434,10 +450,11 @@ namespace SharpChat { else if(string.IsNullOrEmpty(nickStr)) nickStr = null; - if(nickStr != null && Context.Users.Get(nickStr) != null) { - user.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); - break; - } + lock(Context.UsersAccess) + if(!string.IsNullOrWhiteSpace(nickStr) && Context.Users.Any(u => u.NameEquals(nickStr))) { + user.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); + break; + } string previousName = targetUser == user ? (targetUser.Nickname ?? targetUser.Username) : null; targetUser.Nickname = nickStr; @@ -450,10 +467,13 @@ namespace SharpChat { break; } - ChatUser whisperUser = Context.Users.Get(parts[1]); + ChatUser whisperUser; + string whisperUserStr = parts.ElementAtOrDefault(1); + lock(Context.UsersAccess) + whisperUser = Context.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr)); if(whisperUser == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts[1])); + user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, whisperUserStr)); break; } @@ -499,7 +519,9 @@ namespace SharpChat { string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty; if(!string.IsNullOrEmpty(whoChanStr)) { - ChatChannel whoChan = Context.Channels.Get(whoChanStr); + ChatChannel whoChan; + lock(Context.ChannelsAccess) + whoChan = Context.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr)); if(whoChan == null) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); @@ -527,16 +549,17 @@ namespace SharpChat { user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB)); } else { - foreach(ChatUser whoUser in Context.Users.All()) { - whoChanSB.Append(@"'); - whoChanSB.Append(whoUser.DisplayName); - whoChanSB.Append(", "); - } + whoChanSB.Append('>'); + whoChanSB.Append(whoUser.DisplayName); + whoChanSB.Append(", "); + } if(whoChanSB.Length > 2) whoChanSB.Length -= 2; @@ -563,10 +586,13 @@ namespace SharpChat { if(parts.Length < 2) break; - ChatChannel joinChan = Context.Channels.Get(parts[1]); + string joinChanStr = parts.ElementAtOrDefault(1); + ChatChannel joinChan; + lock(Context.ChannelsAccess) + joinChan = Context.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); if(joinChan == null) { - user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, parts[1])); + user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); user.ForceChannel(); break; } @@ -596,25 +622,34 @@ namespace SharpChat { } string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1)); - ChatChannel createChan = new() { - Name = createChanName, - IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent), - Rank = createChanHierarchy, - Owner = user, - }; - try { - Context.Channels.Add(createChan); - } catch(ChannelExistException) { - user.Send(new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChan.Name)); - break; - } catch(ChannelInvalidNameException) { + if(!ChatChannel.CheckName(createChanName)) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID)); break; } - Context.SwitchChannel(user, createChan, createChan.Password); - user.Send(new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name)); + lock(Context.ChannelsAccess) { + if(Context.Channels.Any(c => c.NameEquals(createChanName))) { + user.Send(new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); + break; + } + + ChatChannel createChan = new() { + Name = createChanName, + IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent), + Rank = createChanHierarchy, + Owner = user, + }; + + Context.Channels.Add(createChan); + lock(Context.UsersAccess) { + foreach(ChatUser ccu in Context.Users.Where(u => u.Rank >= channel.Rank)) + ccu.Send(new ChannelCreatePacket(channel)); + } + + Context.SwitchChannel(user, createChan, createChan.Password); + user.Send(new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name)); + } break; case "delchan": // delete a channel if(parts.Length < 2 || string.IsNullOrWhiteSpace(parts[1])) { @@ -623,7 +658,9 @@ namespace SharpChat { } string delChanName = string.Join('_', parts.Skip(1)); - ChatChannel delChan = Context.Channels.Get(delChanName); + ChatChannel delChan; + lock(Context.ChannelsAccess) + delChan = Context.Channels.FirstOrDefault(c => c.NameEquals(delChanName)); if(delChan == null) { user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName)); @@ -635,7 +672,8 @@ namespace SharpChat { break; } - Context.Channels.Remove(delChan); + lock(Context.ChannelsAccess) + Context.RemoveChannel(delChan); user.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name)); break; case "password": // set a password on the channel @@ -650,7 +688,8 @@ namespace SharpChat { if(string.IsNullOrWhiteSpace(chanPass)) chanPass = string.Empty; - Context.Channels.Update(channel, password: chanPass); + lock(Context.ChannelsAccess) + Context.UpdateChannel(channel, password: chanPass); user.Send(new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false)); break; case "privilege": // sets a minimum hierarchy requirement on the channel @@ -666,7 +705,8 @@ namespace SharpChat { break; } - Context.Channels.Update(channel, hierarchy: chanHierarchy); + lock(Context.ChannelsAccess) + Context.UpdateChannel(channel, hierarchy: chanHierarchy); user.Send(new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false)); break; @@ -691,14 +731,16 @@ namespace SharpChat { break; } - IChatEvent delMsg = Context.Events.Get(delSeqId); + lock(Context.EventsAccess) { + IChatEvent delMsg = Context.Events.GetEvent(delSeqId); - if(delMsg == null || delMsg.Sender.Rank > user.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != user.UserId)) { - user.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR)); - break; + if(delMsg == null || delMsg.Sender.Rank > user.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != user.UserId)) { + user.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR)); + break; + } + + Context.Events.RemoveEvent(delMsg); } - - Context.Events.Remove(delMsg); break; case "kick": // kick a user from the server case "ban": // ban a user from the server, this differs from /kick in that it adds all remote address to the ip banlist @@ -714,10 +756,11 @@ namespace SharpChat { int banReasonIndex = 2; ChatUser banUser = null; - if(banUserTarget == null || (banUser = Context.Users.Get(banUserTarget)) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); - break; - } + lock(Context.UsersAccess) + if(banUserTarget == null || (banUser = Context.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) { + user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); + break; + } if(banUser == user || banUser.Rank >= user.Rank) { user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); @@ -776,10 +819,13 @@ namespace SharpChat { break; } - ChatUser unbanUser = Context.Users.Get(unbanUserTarget); + ChatUser unbanUser; + lock(Context.UsersAccess) + unbanUser = Context.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { unbanUserTargetIsName = false; - unbanUser = Context.Users.Get(unbanUserId); + lock(Context.UsersAccess) + unbanUser = Context.Users.FirstOrDefault(u => u.UserId == unbanUserId); } if(unbanUser != null) @@ -849,12 +895,14 @@ namespace SharpChat { break; } + string silUserStr = parts.ElementAtOrDefault(1); ChatUser silUser; - if(parts.Length < 2 || (silUser = Context.Users.Get(parts[1])) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); - break; - } + lock(Context.UsersAccess) + if(parts.Length < 2 || (silUser = Context.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) { + user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : silUserStr)); + break; + } if(silUser == user) { user.Send(new LegacyCommandResponse(LCR.SILENCE_SELF)); @@ -892,12 +940,14 @@ namespace SharpChat { break; } + string unsilUserStr = parts.ElementAtOrDefault(1); ChatUser unsilUser; - if(parts.Length < 2 || (unsilUser = Context.Users.Get(parts[1])) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); - break; - } + lock(Context.UsersAccess) + if(parts.Length < 2 || (unsilUser = Context.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) { + user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : unsilUserStr)); + break; + } if(unsilUser.Rank >= user.Rank) { user.Send(new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY)); @@ -920,11 +970,14 @@ namespace SharpChat { break; } + string ipUserStr = parts.ElementAtOrDefault(1); ChatUser ipUser; - if(parts.Length < 2 || (ipUser = Context.Users.Get(parts[1])) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); - break; - } + + lock(Context.UsersAccess) + if(parts.Length < 2 || (ipUser = Context.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) { + user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : ipUserStr)); + break; + } foreach(IPAddress ip in ipUser.RemoteAddresses.Distinct().ToArray()) user.Send(new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip)); @@ -942,7 +995,7 @@ namespace SharpChat { IsShuttingDown = true; if(commandName == "restart") - lock(SessionsLock) + lock(SessionsAccess) Sessions.ForEach(s => s.PrepareForRestart()); Context.Update(); @@ -971,11 +1024,10 @@ namespace SharpChat { return; IsDisposed = true; - lock(SessionsLock) + lock(SessionsAccess) Sessions.ForEach(s => s.Dispose()); Server?.Dispose(); - Context?.Dispose(); HttpClient?.Dispose(); } } diff --git a/SharpChat/UserManager.cs b/SharpChat/UserManager.cs deleted file mode 100644 index a086ac3..0000000 --- a/SharpChat/UserManager.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SharpChat { - public class UserManager : IDisposable { - private readonly List Users = new(); - - public readonly ChatContext Context; - - public bool IsDisposed { get; private set; } - - public UserManager(ChatContext context) { - Context = context; - } - - public void Add(ChatUser user) { - if(user == null) - throw new ArgumentNullException(nameof(user)); - - lock(Users) - if(!Contains(user)) - Users.Add(user); - } - - public void Remove(ChatUser user) { - if(user == null) - return; - - lock(Users) - Users.Remove(user); - } - - public bool Contains(ChatUser user) { - if(user == null) - return false; - - lock(Users) - return Users.Contains(user) || Users.Any(x => x.UserId == user.UserId || x.Username.ToLowerInvariant() == user.Username.ToLowerInvariant()); - } - - public ChatUser Get(long userId) { - lock(Users) - return Users.FirstOrDefault(x => x.UserId == userId); - } - - public ChatUser Get(string username, bool includeNickName = true, bool includeDisplayName = true) { - if(string.IsNullOrWhiteSpace(username)) - return null; - username = username.ToLowerInvariant(); - - lock(Users) - return Users.FirstOrDefault(x => x.Username.ToLowerInvariant() == username - || (includeNickName && x.Nickname?.ToLowerInvariant() == username) - || (includeDisplayName && x.DisplayName.ToLowerInvariant() == username)); - } - - public IEnumerable Where(Func selector) { - return Users.Where(selector); - } - - public IEnumerable OfHierarchy(int hierarchy) { - lock(Users) - return Users.Where(u => u.Rank >= hierarchy).ToList(); - } - - public IEnumerable WithActiveConnections() { - lock(Users) - return Users.Where(u => u.HasSessions).ToList(); - } - - public IEnumerable All() { - lock(Users) - return Users.ToList(); - } - - ~UserManager() { - DoDispose(); - } - - public void Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { - if(IsDisposed) - return; - IsDisposed = true; - - Users.Clear(); - } - } -}