Marginal improvements to cross thread access.

This commit is contained in:
flash 2023-02-10 07:07:59 +01:00
parent e1e3def62c
commit c21605cf3b
16 changed files with 586 additions and 814 deletions

View file

@ -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<ChatChannel> 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<ChatChannel> GetUser(ChatUser user) {
if(user == null)
return null;
return Channels.Where(x => x.HasUser(user));
}
public IEnumerable<ChatChannel> 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();
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -24,8 +25,7 @@ namespace SharpChat {
} }
public bool HasUser(ChatUser user) { public bool HasUser(ChatUser user) {
lock(Users) return Users.Contains(user);
return Users.Contains(user);
} }
public void UserJoin(ChatUser user) { public void UserJoin(ChatUser user) {
@ -35,36 +35,29 @@ namespace SharpChat {
user.JoinChannel(this); user.JoinChannel(this);
} }
lock(Users) { if(!HasUser(user))
if(!HasUser(user)) Users.Add(user);
Users.Add(user);
}
} }
public void UserLeave(ChatUser user) { public void UserLeave(ChatUser user) {
lock(Users) Users.Remove(user);
Users.Remove(user);
if(user.InChannel(this)) if(user.InChannel(this))
user.LeaveChannel(this); user.LeaveChannel(this);
} }
public void Send(IServerPacket packet) { public void Send(IServerPacket packet) {
lock(Users) { foreach(ChatUser user in Users)
foreach(ChatUser user in Users) user.Send(packet);
user.Send(packet);
}
} }
public IEnumerable<ChatUser> GetUsers(IEnumerable<ChatUser> exclude = null) { public IEnumerable<ChatUser> GetUsers(IEnumerable<ChatUser> exclude = null) {
lock(Users) { IEnumerable<ChatUser> users = Users.OrderByDescending(x => x.Rank);
IEnumerable<ChatUser> users = Users.OrderByDescending(x => x.Rank);
if(exclude != null) if(exclude != null)
users = users.Except(exclude); users = users.Except(exclude);
return users.ToList(); return users.ToList();
}
} }
public string Pack() { public string Pack() {
@ -78,5 +71,21 @@ namespace SharpChat {
return sb.ToString(); 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 == '-';
}
} }
} }

View file

@ -1,24 +1,39 @@
using SharpChat.Events; using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Packet; using SharpChat.Packet;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace SharpChat { namespace SharpChat {
public class ChatContext : IDisposable { public class ChatContext {
public bool IsDisposed { get; private set; } public HashSet<ChatChannel> Channels { get; } = new();
public readonly object ChannelsAccess = new();
public ChannelManager Channels { get; } public HashSet<ChatUser> Users { get; } = new();
public UserManager Users { get; } public readonly object UsersAccess = new();
public ChatEventManager Events { get; }
public ChatContext() { public IEventStorage Events { get; }
Users = new(this); public readonly object EventsAccess = new();
Channels = new(this);
Events = new(this); public ChatContext(IEventStorage evtStore) {
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
} }
public void Update() { public void Update() {
CheckPings(); lock(UsersAccess)
foreach(ChatUser user in Users) {
IEnumerable<ChatUserSession> 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) { 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) { public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess, int maxMsgLength) {
if(!chan.HasUser(user)) { lock(EventsAccess) {
chan.Send(new UserConnectPacket(DateTimeOffset.Now, user)); if(!chan.HasUser(user)) {
Events.Add(new UserConnectEvent(DateTimeOffset.Now, user, chan)); 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<IChatEvent> 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) { public void UserLeave(ChatChannel chan, ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
@ -65,11 +81,14 @@ namespace SharpChat {
} }
if(chan.IsTemporary && chan.Owner == user) if(chan.IsTemporary && chan.Owner == user)
Channels.Remove(chan); lock(ChannelsAccess)
RemoveChannel(chan);
chan.UserLeave(user); lock(EventsAccess) {
chan.Send(new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); chan.UserLeave(user);
Events.Add(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); 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) { public void SwitchChannel(ChatUser user, ChatChannel chan, string password) {
@ -97,70 +116,96 @@ namespace SharpChat {
} }
public void ForceChannelSwitch(ChatUser user, ChatChannel chan) { public void ForceChannelSwitch(ChatUser user, ChatChannel chan) {
if(!Channels.Contains(chan)) lock(ChannelsAccess)
return; if(!Channels.Contains(chan))
return;
ChatChannel oldChan = user.CurrentChannel; ChatChannel oldChan = user.CurrentChannel;
oldChan.Send(new UserChannelLeavePacket(user)); lock(EventsAccess) {
Events.Add(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan)); oldChan.Send(new UserChannelLeavePacket(user));
chan.Send(new UserChannelJoinPacket(user)); Events.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan));
Events.Add(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan)); chan.Send(new UserChannelJoinPacket(user));
Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan));
user.Send(new ContextClearPacket(chan, ContextClearMode.MessagesUsers)); user.Send(new ContextClearPacket(chan, ContextClearMode.MessagesUsers));
user.Send(new ContextUsersPacket(chan.GetUsers(new[] { user }))); user.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
IEnumerable<IChatEvent> msgs = Events.GetTargetLog(chan); foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name))
user.Send(new ContextMessagePacket(msg));
foreach(IChatEvent msg in msgs) user.ForceChannel(chan);
user.Send(new ContextMessagePacket(msg)); oldChan.UserLeave(user);
chan.UserJoin(user);
user.ForceChannel(chan); }
oldChan.UserLeave(user);
chan.UserJoin(user);
if(oldChan.IsTemporary && oldChan.Owner == user) if(oldChan.IsTemporary && oldChan.Owner == user)
Channels.Remove(oldChan); lock(ChannelsAccess)
} RemoveChannel(oldChan);
public void CheckPings() {
lock(Users)
foreach(ChatUser user in Users.All()) {
IEnumerable<ChatUserSession> 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 Send(IServerPacket packet) { public void Send(IServerPacket packet) {
foreach(ChatUser user in Users.All()) lock(UsersAccess)
user.Send(packet); foreach(ChatUser user in Users)
user.Send(packet);
} }
~ChatContext() { public void UpdateChannel(ChatChannel channel, string name = null, bool? temporary = null, int? hierarchy = null, string password = null) {
DoDispose(); 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() { public void RemoveChannel(ChatChannel channel) {
DoDispose(); if(channel == null || !Channels.Any())
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return; return;
IsDisposed = true;
Events?.Dispose(); ChatChannel defaultChannel = Channels.FirstOrDefault();
Channels?.Dispose(); if(defaultChannel == null)
Users?.Dispose(); 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));
} }
} }
} }

View file

@ -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<IChatEvent> 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<IChatEvent> 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<IChatEvent> 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<IChatEvent>();
}
~ChatEventManager() {
DoDispose();
}
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Events?.Clear();
}
}
}

View file

@ -17,17 +17,15 @@ namespace SharpChat {
public ChatRateLimitState State { public ChatRateLimitState State {
get { get {
lock(TimePoints) { if(TimePoints.Count == FLOOD_PROTECTION_AMOUNT) {
if(TimePoints.Count == FLOOD_PROTECTION_AMOUNT) { if((TimePoints.Last() - TimePoints.First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD)
if((TimePoints.Last() - TimePoints.First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD) return ChatRateLimitState.Kick;
return ChatRateLimitState.Kick;
if((TimePoints.Last() - TimePoints.Skip(5).First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD) if((TimePoints.Last() - TimePoints.Skip(5).First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD)
return ChatRateLimitState.Warning; return ChatRateLimitState.Warning;
}
return ChatRateLimitState.None;
} }
return ChatRateLimitState.None;
} }
} }
@ -35,12 +33,10 @@ namespace SharpChat {
if(!dto.HasValue) if(!dto.HasValue)
dto = DateTimeOffset.Now; dto = DateTimeOffset.Now;
lock(TimePoints) { if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT)
if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT) TimePoints.Dequeue();
TimePoints.Dequeue();
TimePoints.Enqueue(dto.Value); TimePoints.Enqueue(dto.Value);
}
} }
} }
} }

View file

@ -2,7 +2,6 @@
using SharpChat.Packet; using SharpChat.Packet;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
@ -23,10 +22,6 @@ namespace SharpChat {
public bool HasFloodProtection public bool HasFloodProtection
=> Rank < RANK_NO_FLOOD; => Rank < RANK_NO_FLOOD;
public bool Equals([AllowNull] BasicUser other) {
return UserId == other.UserId;
}
public string DisplayName { public string DisplayName {
get { get {
StringBuilder sb = new(); StringBuilder sb = new();
@ -71,6 +66,18 @@ namespace SharpChat {
return sb.ToString(); 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 { public class ChatUser : BasicUser, IPacketTarget {
@ -83,12 +90,7 @@ namespace SharpChat {
public string TargetName => "@log"; public string TargetName => "@log";
public ChatChannel Channel { public ChatChannel Channel => Channels.FirstOrDefault();
get {
lock(Channels)
return Channels.FirstOrDefault();
}
}
// This needs to be a session thing // This needs to be a session thing
public ChatChannel CurrentChannel { get; private set; } public ChatChannel CurrentChannel { get; private set; }
@ -96,26 +98,11 @@ namespace SharpChat {
public bool IsSilenced public bool IsSilenced
=> DateTimeOffset.UtcNow - SilencedUntil <= TimeSpan.Zero; => DateTimeOffset.UtcNow - SilencedUntil <= TimeSpan.Zero;
public bool HasSessions { public bool HasSessions => Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Any();
get {
lock(Sessions)
return Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Any();
}
}
public int SessionCount { public int SessionCount => Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Count();
get {
lock(Sessions)
return Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Count();
}
}
public IEnumerable<IPAddress> RemoteAddresses { public IEnumerable<IPAddress> RemoteAddresses => Sessions.Select(c => c.RemoteAddress);
get {
lock(Sessions)
return Sessions.Select(c => c.RemoteAddress);
}
}
public ChatUser() { public ChatUser() {
} }
@ -140,17 +127,14 @@ namespace SharpChat {
} }
public void Send(IServerPacket packet) { public void Send(IServerPacket packet) {
lock(Sessions) foreach(ChatUserSession conn in Sessions)
foreach(ChatUserSession conn in Sessions) conn.Send(packet);
conn.Send(packet);
} }
public void Close() { public void Close() {
lock(Sessions) { foreach(ChatUserSession conn in Sessions)
foreach(ChatUserSession conn in Sessions) conn.Dispose();
conn.Dispose(); Sessions.Clear();
Sessions.Clear();
}
} }
public void ForceChannel(ChatChannel chan = null) { public void ForceChannel(ChatChannel chan = null) {
@ -158,36 +142,28 @@ namespace SharpChat {
} }
public void FocusChannel(ChatChannel chan) { public void FocusChannel(ChatChannel chan) {
lock(Channels) { if(InChannel(chan))
if(InChannel(chan)) CurrentChannel = chan;
CurrentChannel = chan;
}
} }
public bool InChannel(ChatChannel chan) { public bool InChannel(ChatChannel chan) {
lock(Channels) return Channels.Contains(chan);
return Channels.Contains(chan);
} }
public void JoinChannel(ChatChannel chan) { public void JoinChannel(ChatChannel chan) {
lock(Channels) { if(!InChannel(chan)) {
if(!InChannel(chan)) { Channels.Add(chan);
Channels.Add(chan); CurrentChannel = chan;
CurrentChannel = chan;
}
} }
} }
public void LeaveChannel(ChatChannel chan) { public void LeaveChannel(ChatChannel chan) {
lock(Channels) { Channels.Remove(chan);
Channels.Remove(chan); CurrentChannel = Channels.FirstOrDefault();
CurrentChannel = Channels.FirstOrDefault();
}
} }
public IEnumerable<ChatChannel> GetChannels() { public IEnumerable<ChatChannel> GetChannels() {
lock(Channels) return Channels.ToList();
return Channels.ToList();
} }
public void AddSession(ChatUserSession sess) { public void AddSession(ChatUserSession sess) {
@ -195,8 +171,7 @@ namespace SharpChat {
return; return;
sess.User = this; sess.User = this;
lock(Sessions) Sessions.Add(sess);
Sessions.Add(sess);
} }
public void RemoveSession(ChatUserSession sess) { public void RemoveSession(ChatUserSession sess) {
@ -205,13 +180,17 @@ namespace SharpChat {
if(!sess.IsDisposed) // this could be possible if(!sess.IsDisposed) // this could be possible
sess.User = null; sess.User = null;
lock(Sessions) Sessions.Remove(sess);
Sessions.Remove(sess);
} }
public IEnumerable<ChatUserSession> GetDeadSessions() { public IEnumerable<ChatUserSession> 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);
} }
} }
} }

View file

@ -6,14 +6,14 @@ namespace SharpChat.Config {
private string Name { get; } private string Name { get; }
private TimeSpan Lifetime { get; } private TimeSpan Lifetime { get; }
private T Fallback { get; } private T Fallback { get; }
private object Sync { get; } = new(); private object ConfigAccess { get; } = new();
private object CurrentValue { get; set; } private object CurrentValue { get; set; }
private DateTimeOffset LastRead { get; set; } private DateTimeOffset LastRead { get; set; }
public T Value { public T Value {
get { get {
lock(Sync) { lock(ConfigAccess) { // this lock doesn't really make sense since it doesn't affect other config calls
DateTimeOffset now = DateTimeOffset.Now; DateTimeOffset now = DateTimeOffset.Now;
if((now - LastRead) >= Lifetime) { if((now - LastRead) >= Lifetime) {
LastRead = now; LastRead = now;
@ -37,9 +37,7 @@ namespace SharpChat.Config {
} }
public void Refresh() { public void Refresh() {
lock(Sync) { LastRead = DateTimeOffset.MinValue;
LastRead = DateTimeOffset.MinValue;
}
} }
public override string ToString() { public override string ToString() {

View file

@ -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<IChatEvent> GetTargetEventLog(string target, int amount = 20, int offset = 0);
}
}

View file

@ -1,113 +1,22 @@
using MySqlConnector; using MySqlConnector;
using SharpChat.Config;
using SharpChat.Events; using SharpChat.Events;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
namespace SharpChat { namespace SharpChat.EventStorage {
public static partial class Database { public partial class MariaDBEventStorage : IEventStorage {
private static string ConnectionString = null; private string ConnectionString { get; }
public static bool HasDatabase public MariaDBEventStorage(string connString) {
=> !string.IsNullOrWhiteSpace(ConnectionString); ConnectionString = connString ?? throw new ArgumentNullException(nameof(connString));
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 static void Init(string host, string username, string password, string database) { public void AddEvent(IChatEvent evt) {
ConnectionString = new MySqlConnectionStringBuilder { if(evt == null)
Server = host, throw new ArgumentNullException(nameof(evt));
UserID = username,
Password = password,
Database = database,
OldGuids = false,
TreatTinyAsBoolean = false,
CharacterSet = "utf8mb4",
SslMode = MySqlSslMode.None,
ForceSynchronous = true,
ConnectionTimeout = 5,
}.ToString();
RunMigrations();
}
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) if(evt.SequenceId < 1)
evt.SequenceId = SharpId.Next(); evt.SequenceId = SharpId.Next();
@ -131,67 +40,7 @@ namespace SharpChat {
); );
} }
public static void DeleteEvent(IChatEvent evt) { public IChatEvent GetEvent(long seqId) {
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<IChatEvent> GetEvents(IPacketTarget target, int amount, int offset) {
List<IChatEvent> 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) {
try { try {
using MySqlDataReader reader = RunQuery( using MySqlDataReader reader = RunQuery(
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`" "SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
@ -213,5 +62,66 @@ namespace SharpChat {
return null; 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<IChatEvent> GetTargetEventLog(string target, int amount = 20, int offset = 0) {
List<IChatEvent> 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)
);
}
} }
} }

View file

@ -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;
}
}
}

View file

@ -1,9 +1,9 @@
using MySqlConnector; using MySqlConnector;
using System; using System;
namespace SharpChat { namespace SharpChat.EventStorage {
public static partial class Database { public partial class MariaDBEventStorage {
private static void DoMigration(string name, Action action) { private void DoMigration(string name, Action action) {
bool done = (long)RunQueryValue( bool done = (long)RunQueryValue(
"SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name", "SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name",
new MySqlParameter("name", name) new MySqlParameter("name", name)
@ -18,7 +18,7 @@ namespace SharpChat {
} }
} }
private static void RunMigrations() { public void RunMigrations() {
RunCommand( RunCommand(
"CREATE TABLE IF NOT EXISTS `sqc_migrations` (" "CREATE TABLE IF NOT EXISTS `sqc_migrations` ("
+ "`migration_name` VARCHAR(255) NOT NULL," + "`migration_name` VARCHAR(255) NOT NULL,"
@ -31,7 +31,7 @@ namespace SharpChat {
DoMigration("create_events_table", CreateEventsTable); DoMigration("create_events_table", CreateEventsTable);
} }
private static void CreateEventsTable() { private void CreateEventsTable() {
RunCommand( RunCommand(
"CREATE TABLE `sqc_events` (" "CREATE TABLE `sqc_events` ("
+ "`event_id` BIGINT(20) NOT NULL," + "`event_id` BIGINT(20) NOT NULL,"

View file

@ -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<long, IChatEvent> 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<IChatEvent> GetTargetEventLog(string target, int amount = 20, int offset = 0) {
IEnumerable<IChatEvent> 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();
}
}
}

View file

@ -1,4 +1,5 @@
using SharpChat.Config; using SharpChat.Config;
using SharpChat.EventStorage;
using SharpChat.Misuzu; using SharpChat.Misuzu;
using System; using System;
using System.IO; using System.IO;
@ -46,8 +47,6 @@ namespace SharpChat {
using IConfig config = new StreamConfig(configFile); using IConfig config = new StreamConfig(configFile);
Database.Init(config.ScopeTo("mariadb"));
if(hasCancelled) return; if(hasCancelled) return;
using HttpClient httpClient = new(new HttpClientHandler() { using HttpClient httpClient = new(new HttpClientHandler() {
@ -61,7 +60,18 @@ namespace SharpChat {
if(hasCancelled) return; 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); scs.Listen(mre);
mre.WaitOne(); mre.WaitOne();

View file

@ -3,28 +3,23 @@ using System.Security.Cryptography;
namespace SharpChat { namespace SharpChat {
public static class RNG { public static class RNG {
private static object Lock { get; } = new();
private static Random NormalRandom { get; } = new(); private static Random NormalRandom { get; } = new();
private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create(); private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create();
public static int Next() { public static int Next() {
lock(Lock) return NormalRandom.Next();
return NormalRandom.Next();
} }
public static int Next(int max) { public static int Next(int max) {
lock(Lock) return NormalRandom.Next(max);
return NormalRandom.Next(max);
} }
public static int Next(int min, int 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) { public static void NextBytes(byte[] buffer) {
lock(Lock) SecureRandom.GetBytes(buffer);
SecureRandom.GetBytes(buffer);
} }
} }
} }

View file

@ -2,6 +2,7 @@
using SharpChat.Commands; using SharpChat.Commands;
using SharpChat.Config; using SharpChat.Config;
using SharpChat.Events; using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Misuzu; using SharpChat.Misuzu;
using SharpChat.Packet; using SharpChat.Packet;
using System; using System;
@ -45,17 +46,19 @@ namespace SharpChat {
}; };
public List<ChatUserSession> Sessions { get; } = new List<ChatUserSession>(); public List<ChatUserSession> Sessions { get; } = new List<ChatUserSession>();
private object SessionsLock { get; } = new object(); private object SessionsAccess { get; } = new object();
public ChatUserSession GetSession(IWebSocketConnection conn) { public ChatUserSession GetSession(IWebSocketConnection conn) {
lock(SessionsLock) lock(SessionsAccess)
return Sessions.FirstOrDefault(x => x.Connection == conn); return Sessions.FirstOrDefault(x => x.Connection == conn);
} }
private ManualResetEvent Shutdown { get; set; } private ManualResetEvent Shutdown { get; set; }
private bool IsShuttingDown = false; 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..."); Logger.Write("Initialising Sock Chat server...");
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
@ -65,7 +68,7 @@ namespace SharpChat {
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS); MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH); FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
Context = new ChatContext(); Context = new ChatContext(evtStore);
string[] channelNames = config.ReadValue("channels", new[] { "lounge" }); string[] channelNames = config.ReadValue("channels", new[] { "lounge" });
@ -82,6 +85,8 @@ namespace SharpChat {
channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0); channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0);
Context.Channels.Add(channelInfo); Context.Channels.Add(channelInfo);
DefaultChannel ??= channelInfo;
} }
ushort port = config.SafeReadValue("port", DEFAULT_PORT); ushort port = config.SafeReadValue("port", DEFAULT_PORT);
@ -107,7 +112,9 @@ namespace SharpChat {
} }
private void OnOpen(IWebSocketConnection conn) { 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)) if(!Sessions.Any(x => x.Connection == conn))
Sessions.Add(new ChatUserSession(conn)); Sessions.Add(new ChatUserSession(conn));
} }
@ -116,6 +123,8 @@ namespace SharpChat {
} }
private void OnClose(IWebSocketConnection conn) { private void OnClose(IWebSocketConnection conn) {
Logger.Write($"Connection closed from {conn.ConnectionInfo.ClientIpAddress}:{conn.ConnectionInfo.ClientPort}");
ChatUserSession sess = GetSession(conn); ChatUserSession sess = GetSession(conn);
// Remove connection from user // Remove connection from user
@ -133,7 +142,7 @@ namespace SharpChat {
Context.Update(); Context.Update();
// Remove connection from server // Remove connection from server
lock(SessionsLock) lock(SessionsAccess)
Sessions.Remove(sess); Sessions.Remove(sess);
sess?.Dispose(); sess?.Dispose();
@ -195,10 +204,12 @@ namespace SharpChat {
lock(BumpAccess) { lock(BumpAccess) {
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
(string, string)[] bumpList = Context.Users (string, string)[] bumpList;
.Where(u => u.HasSessions && u.Status == ChatUserStatus.Online) lock(Context.UsersAccess)
.Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty)) bumpList = Context.Users
.ToArray(); .Where(u => u.HasSessions && u.Status == ChatUserStatus.Online)
.Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty))
.ToArray();
if(bumpList.Any()) if(bumpList.Any())
Task.Run(async () => { Task.Run(async () => {
@ -279,38 +290,40 @@ namespace SharpChat {
return; return;
} }
ChatUser aUser = Context.Users.Get(fai.UserId); lock(Context.UsersAccess) {
ChatUser aUser = Context.Users.FirstOrDefault(u => u.UserId == fai.UserId);
if(aUser == null) if(aUser == null)
aUser = new ChatUser(fai); aUser = new ChatUser(fai);
else { else {
aUser.ApplyAuth(fai); aUser.ApplyAuth(fai);
aUser.Channel?.Send(new UserUpdatePacket(aUser)); 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<string> 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<string> 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(); }).Wait();
break; break;
@ -370,8 +383,10 @@ namespace SharpChat {
Text = messageText, Text = messageText,
}; };
Context.Events.Add(message); lock(Context.EventsAccess) {
mChannel.Send(new ChatMessageAddPacket(message)); Context.Events.AddEvent(message);
mChannel.Send(new ChatMessageAddPacket(message));
}
break; break;
} }
} }
@ -408,7 +423,8 @@ namespace SharpChat {
int offset = 1; int offset = 1;
if(setOthersNick && parts.Length > 1 && long.TryParse(parts[1], out long targetUserId) && targetUserId > 0) { 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; offset = 2;
} }
@ -434,10 +450,11 @@ namespace SharpChat {
else if(string.IsNullOrEmpty(nickStr)) else if(string.IsNullOrEmpty(nickStr))
nickStr = null; nickStr = null;
if(nickStr != null && Context.Users.Get(nickStr) != null) { lock(Context.UsersAccess)
user.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); if(!string.IsNullOrWhiteSpace(nickStr) && Context.Users.Any(u => u.NameEquals(nickStr))) {
break; user.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr));
} break;
}
string previousName = targetUser == user ? (targetUser.Nickname ?? targetUser.Username) : null; string previousName = targetUser == user ? (targetUser.Nickname ?? targetUser.Username) : null;
targetUser.Nickname = nickStr; targetUser.Nickname = nickStr;
@ -450,10 +467,13 @@ namespace SharpChat {
break; 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) { 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; break;
} }
@ -499,7 +519,9 @@ namespace SharpChat {
string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty; string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty;
if(!string.IsNullOrEmpty(whoChanStr)) { 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) { if(whoChan == null) {
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); 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)); user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
} else { } else {
foreach(ChatUser whoUser in Context.Users.All()) { lock(Context.UsersAccess)
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);"""); foreach(ChatUser whoUser in Context.Users) {
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(whoUser == user) if(whoUser == user)
whoChanSB.Append(@" style=""font-weight: bold;"""); whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append('>'); whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName); whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, "); whoChanSB.Append("</a>, ");
} }
if(whoChanSB.Length > 2) if(whoChanSB.Length > 2)
whoChanSB.Length -= 2; whoChanSB.Length -= 2;
@ -563,10 +586,13 @@ namespace SharpChat {
if(parts.Length < 2) if(parts.Length < 2)
break; 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) { 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(); user.ForceChannel();
break; break;
} }
@ -596,25 +622,34 @@ namespace SharpChat {
} }
string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1)); string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1));
ChatChannel createChan = new() {
Name = createChanName,
IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent),
Rank = createChanHierarchy,
Owner = user,
};
try { if(!ChatChannel.CheckName(createChanName)) {
Context.Channels.Add(createChan);
} catch(ChannelExistException) {
user.Send(new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChan.Name));
break;
} catch(ChannelInvalidNameException) {
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID)); user.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID));
break; break;
} }
Context.SwitchChannel(user, createChan, createChan.Password); lock(Context.ChannelsAccess) {
user.Send(new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name)); 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; break;
case "delchan": // delete a channel case "delchan": // delete a channel
if(parts.Length < 2 || string.IsNullOrWhiteSpace(parts[1])) { if(parts.Length < 2 || string.IsNullOrWhiteSpace(parts[1])) {
@ -623,7 +658,9 @@ namespace SharpChat {
} }
string delChanName = string.Join('_', parts.Skip(1)); 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) { if(delChan == null) {
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName)); user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName));
@ -635,7 +672,8 @@ namespace SharpChat {
break; break;
} }
Context.Channels.Remove(delChan); lock(Context.ChannelsAccess)
Context.RemoveChannel(delChan);
user.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name)); user.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name));
break; break;
case "password": // set a password on the channel case "password": // set a password on the channel
@ -650,7 +688,8 @@ namespace SharpChat {
if(string.IsNullOrWhiteSpace(chanPass)) if(string.IsNullOrWhiteSpace(chanPass))
chanPass = string.Empty; 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)); user.Send(new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false));
break; break;
case "privilege": // sets a minimum hierarchy requirement on the channel case "privilege": // sets a minimum hierarchy requirement on the channel
@ -666,7 +705,8 @@ namespace SharpChat {
break; break;
} }
Context.Channels.Update(channel, hierarchy: chanHierarchy); lock(Context.ChannelsAccess)
Context.UpdateChannel(channel, hierarchy: chanHierarchy);
user.Send(new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false)); user.Send(new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false));
break; break;
@ -691,14 +731,16 @@ namespace SharpChat {
break; 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)) { if(delMsg == null || delMsg.Sender.Rank > user.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != user.UserId)) {
user.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR)); user.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR));
break; break;
}
Context.Events.RemoveEvent(delMsg);
} }
Context.Events.Remove(delMsg);
break; break;
case "kick": // kick a user from the server 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 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; int banReasonIndex = 2;
ChatUser banUser = null; ChatUser banUser = null;
if(banUserTarget == null || (banUser = Context.Users.Get(banUserTarget)) == null) { lock(Context.UsersAccess)
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); if(banUserTarget == null || (banUser = Context.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) {
break; user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget));
} break;
}
if(banUser == user || banUser.Rank >= user.Rank) { if(banUser == user || banUser.Rank >= user.Rank) {
user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName));
@ -776,10 +819,13 @@ namespace SharpChat {
break; 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)) { if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
unbanUserTargetIsName = false; unbanUserTargetIsName = false;
unbanUser = Context.Users.Get(unbanUserId); lock(Context.UsersAccess)
unbanUser = Context.Users.FirstOrDefault(u => u.UserId == unbanUserId);
} }
if(unbanUser != null) if(unbanUser != null)
@ -849,12 +895,14 @@ namespace SharpChat {
break; break;
} }
string silUserStr = parts.ElementAtOrDefault(1);
ChatUser silUser; ChatUser silUser;
if(parts.Length < 2 || (silUser = Context.Users.Get(parts[1])) == null) { lock(Context.UsersAccess)
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); if(parts.Length < 2 || (silUser = Context.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) {
break; user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : silUserStr));
} break;
}
if(silUser == user) { if(silUser == user) {
user.Send(new LegacyCommandResponse(LCR.SILENCE_SELF)); user.Send(new LegacyCommandResponse(LCR.SILENCE_SELF));
@ -892,12 +940,14 @@ namespace SharpChat {
break; break;
} }
string unsilUserStr = parts.ElementAtOrDefault(1);
ChatUser unsilUser; ChatUser unsilUser;
if(parts.Length < 2 || (unsilUser = Context.Users.Get(parts[1])) == null) { lock(Context.UsersAccess)
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1])); if(parts.Length < 2 || (unsilUser = Context.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) {
break; user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : unsilUserStr));
} break;
}
if(unsilUser.Rank >= user.Rank) { if(unsilUser.Rank >= user.Rank) {
user.Send(new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY)); user.Send(new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY));
@ -920,11 +970,14 @@ namespace SharpChat {
break; break;
} }
string ipUserStr = parts.ElementAtOrDefault(1);
ChatUser ipUser; 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])); lock(Context.UsersAccess)
break; 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()) foreach(IPAddress ip in ipUser.RemoteAddresses.Distinct().ToArray())
user.Send(new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip)); user.Send(new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip));
@ -942,7 +995,7 @@ namespace SharpChat {
IsShuttingDown = true; IsShuttingDown = true;
if(commandName == "restart") if(commandName == "restart")
lock(SessionsLock) lock(SessionsAccess)
Sessions.ForEach(s => s.PrepareForRestart()); Sessions.ForEach(s => s.PrepareForRestart());
Context.Update(); Context.Update();
@ -971,11 +1024,10 @@ namespace SharpChat {
return; return;
IsDisposed = true; IsDisposed = true;
lock(SessionsLock) lock(SessionsAccess)
Sessions.ForEach(s => s.Dispose()); Sessions.ForEach(s => s.Dispose());
Server?.Dispose(); Server?.Dispose();
Context?.Dispose();
HttpClient?.Dispose(); HttpClient?.Dispose();
} }
} }

View file

@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat {
public class UserManager : IDisposable {
private readonly List<ChatUser> 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<ChatUser> Where(Func<ChatUser, bool> selector) {
return Users.Where(selector);
}
public IEnumerable<ChatUser> OfHierarchy(int hierarchy) {
lock(Users)
return Users.Where(u => u.Rank >= hierarchy).ToList();
}
public IEnumerable<ChatUser> WithActiveConnections() {
lock(Users)
return Users.Where(u => u.HasSessions).ToList();
}
public IEnumerable<ChatUser> 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();
}
}
}