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.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<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)
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 == '-';
}
}
}

View file

@ -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<ChatChannel> Channels { get; } = new();
public readonly object ChannelsAccess = new();
public ChannelManager Channels { get; }
public UserManager Users { get; }
public ChatEventManager Events { get; }
public HashSet<ChatUser> 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<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) {
@ -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<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) {
@ -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<IChatEvent> 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<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);
}
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));
}
}
}

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 {
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);
}
}
}

View file

@ -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<IPAddress> RemoteAddresses {
get {
lock(Sessions)
return Sessions.Select(c => c.RemoteAddress);
}
}
public IEnumerable<IPAddress> 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<ChatChannel> 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<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 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() {

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 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<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) {
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<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 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,"

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.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();

View file

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

View file

@ -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<ChatUserSession> Sessions { get; } = new List<ChatUserSession>();
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<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();
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(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
lock(Context.UsersAccess)
foreach(ChatUser whoUser in Context.Users) {
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(whoUser == user)
whoChanSB.Append(@" style=""font-weight: bold;""");
if(whoUser == user)
whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, ");
}
whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, ");
}
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();
}
}

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();
}
}
}