Marginal improvements to cross thread access.
This commit is contained in:
parent
e1e3def62c
commit
c21605cf3b
16 changed files with 586 additions and 814 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
|
@ -24,7 +25,6 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
public bool HasUser(ChatUser user) {
|
||||
lock(Users)
|
||||
return Users.Contains(user);
|
||||
}
|
||||
|
||||
|
@ -35,14 +35,11 @@ namespace SharpChat {
|
|||
user.JoinChannel(this);
|
||||
}
|
||||
|
||||
lock(Users) {
|
||||
if(!HasUser(user))
|
||||
Users.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
public void UserLeave(ChatUser user) {
|
||||
lock(Users)
|
||||
Users.Remove(user);
|
||||
|
||||
if(user.InChannel(this))
|
||||
|
@ -50,14 +47,11 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
public void Send(IServerPacket packet) {
|
||||
lock(Users) {
|
||||
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);
|
||||
|
||||
if(exclude != null)
|
||||
|
@ -65,7 +59,6 @@ namespace SharpChat {
|
|||
|
||||
return users.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
StringBuilder sb = new();
|
||||
|
@ -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 == '-';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,27 +47,28 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess, int maxMsgLength) {
|
||||
lock(EventsAccess) {
|
||||
if(!chan.HasUser(user)) {
|
||||
chan.Send(new UserConnectPacket(DateTimeOffset.Now, user));
|
||||
Events.Add(new UserConnectEvent(DateTimeOffset.Now, user, chan));
|
||||
Events.AddEvent(new UserConnectEvent(DateTimeOffset.Now, user, chan));
|
||||
}
|
||||
|
||||
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)
|
||||
foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name))
|
||||
sess.Send(new ContextMessagePacket(msg));
|
||||
|
||||
sess.Send(new ContextChannelsPacket(Channels.OfHierarchy(user.Rank)));
|
||||
lock(ChannelsAccess)
|
||||
sess.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank)));
|
||||
|
||||
if(!chan.HasUser(user))
|
||||
chan.UserJoin(user);
|
||||
|
||||
if(!Users.Contains(user))
|
||||
lock(UsersAccess)
|
||||
Users.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
public void UserLeave(ChatChannel chan, ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
|
||||
user.Status = ChatUserStatus.Offline;
|
||||
|
@ -65,11 +81,14 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
if(chan.IsTemporary && chan.Owner == user)
|
||||
Channels.Remove(chan);
|
||||
lock(ChannelsAccess)
|
||||
RemoveChannel(chan);
|
||||
|
||||
lock(EventsAccess) {
|
||||
chan.UserLeave(user);
|
||||
chan.Send(new UserDisconnectPacket(DateTimeOffset.Now, user, reason));
|
||||
Events.Add(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, 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) {
|
||||
lock(ChannelsAccess)
|
||||
if(!Channels.Contains(chan))
|
||||
return;
|
||||
|
||||
ChatChannel oldChan = user.CurrentChannel;
|
||||
|
||||
lock(EventsAccess) {
|
||||
oldChan.Send(new UserChannelLeavePacket(user));
|
||||
Events.Add(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan));
|
||||
Events.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan));
|
||||
chan.Send(new UserChannelJoinPacket(user));
|
||||
Events.Add(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan));
|
||||
Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan));
|
||||
|
||||
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 msgs)
|
||||
foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name))
|
||||
user.Send(new ContextMessagePacket(msg));
|
||||
|
||||
user.ForceChannel(chan);
|
||||
oldChan.UserLeave(user);
|
||||
chan.UserJoin(user);
|
||||
}
|
||||
|
||||
if(oldChan.IsTemporary && oldChan.Owner == user)
|
||||
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())
|
||||
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;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
DoDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ 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;
|
||||
|
@ -29,13 +28,11 @@ namespace SharpChat {
|
|||
return ChatRateLimitState.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddTimePoint(DateTimeOffset? dto = null) {
|
||||
if(!dto.HasValue)
|
||||
dto = DateTimeOffset.Now;
|
||||
|
||||
lock(TimePoints) {
|
||||
if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT)
|
||||
TimePoints.Dequeue();
|
||||
|
||||
|
@ -43,4 +40,3 @@ namespace SharpChat {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,53 +127,42 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
public void Send(IServerPacket packet) {
|
||||
lock(Sessions)
|
||||
foreach(ChatUserSession conn in Sessions)
|
||||
conn.Send(packet);
|
||||
}
|
||||
|
||||
public void Close() {
|
||||
lock(Sessions) {
|
||||
foreach(ChatUserSession conn in Sessions)
|
||||
conn.Dispose();
|
||||
Sessions.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void ForceChannel(ChatChannel chan = null) {
|
||||
Send(new UserChannelForceJoinPacket(chan ?? CurrentChannel));
|
||||
}
|
||||
|
||||
public void FocusChannel(ChatChannel chan) {
|
||||
lock(Channels) {
|
||||
if(InChannel(chan))
|
||||
CurrentChannel = chan;
|
||||
}
|
||||
}
|
||||
|
||||
public bool InChannel(ChatChannel chan) {
|
||||
lock(Channels)
|
||||
return Channels.Contains(chan);
|
||||
}
|
||||
|
||||
public void JoinChannel(ChatChannel chan) {
|
||||
lock(Channels) {
|
||||
if(!InChannel(chan)) {
|
||||
Channels.Add(chan);
|
||||
CurrentChannel = chan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LeaveChannel(ChatChannel chan) {
|
||||
lock(Channels) {
|
||||
Channels.Remove(chan);
|
||||
CurrentChannel = Channels.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ChatChannel> GetChannels() {
|
||||
lock(Channels)
|
||||
return Channels.ToList();
|
||||
}
|
||||
|
||||
|
@ -195,7 +171,6 @@ namespace SharpChat {
|
|||
return;
|
||||
sess.User = this;
|
||||
|
||||
lock(Sessions)
|
||||
Sessions.Add(sess);
|
||||
}
|
||||
|
||||
|
@ -205,13 +180,17 @@ namespace SharpChat {
|
|||
if(!sess.IsDisposed) // this could be possible
|
||||
sess.User = null;
|
||||
|
||||
lock(Sessions)
|
||||
Sessions.Remove(sess);
|
||||
}
|
||||
|
||||
public IEnumerable<ChatUserSession> GetDeadSessions() {
|
||||
lock(Sessions)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,10 +37,8 @@ namespace SharpChat.Config {
|
|||
}
|
||||
|
||||
public void Refresh() {
|
||||
lock(Sync) {
|
||||
LastRead = DateTimeOffset.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return Value.ToString();
|
||||
|
|
12
SharpChat/EventStorage/IEventStorage.cs
Normal file
12
SharpChat/EventStorage/IEventStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
82
SharpChat/EventStorage/MariaDBEventStorage_Database.cs
Normal file
82
SharpChat/EventStorage/MariaDBEventStorage_Database.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,"
|
38
SharpChat/EventStorage/VirtualEventStorage.cs
Normal file
38
SharpChat/EventStorage/VirtualEventStorage.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -3,27 +3,22 @@ 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();
|
||||
}
|
||||
|
||||
public static int Next(int max) {
|
||||
lock(Lock)
|
||||
return NormalRandom.Next(max);
|
||||
}
|
||||
|
||||
public static int Next(int min, int max) {
|
||||
lock(Lock)
|
||||
return NormalRandom.Next(min, max);
|
||||
}
|
||||
|
||||
public static void NextBytes(byte[] buffer) {
|
||||
lock(Lock)
|
||||
SecureRandom.GetBytes(buffer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +204,9 @@ namespace SharpChat {
|
|||
|
||||
lock(BumpAccess) {
|
||||
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
|
||||
(string, string)[] bumpList = Context.Users
|
||||
(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();
|
||||
|
@ -279,7 +290,8 @@ 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);
|
||||
|
@ -310,7 +322,8 @@ namespace SharpChat {
|
|||
sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line));
|
||||
}
|
||||
|
||||
Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess, MaxMessageLength);
|
||||
Context.HandleJoin(aUser, DefaultChannel, sess, MaxMessageLength);
|
||||
}
|
||||
}).Wait();
|
||||
break;
|
||||
|
||||
|
@ -370,8 +383,10 @@ namespace SharpChat {
|
|||
Text = messageText,
|
||||
};
|
||||
|
||||
Context.Events.Add(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,7 +450,8 @@ namespace SharpChat {
|
|||
else if(string.IsNullOrEmpty(nickStr))
|
||||
nickStr = null;
|
||||
|
||||
if(nickStr != null && Context.Users.Get(nickStr) != null) {
|
||||
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;
|
||||
}
|
||||
|
@ -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,7 +549,8 @@ namespace SharpChat {
|
|||
|
||||
user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
|
||||
} else {
|
||||
foreach(ChatUser whoUser in Context.Users.All()) {
|
||||
lock(Context.UsersAccess)
|
||||
foreach(ChatUser whoUser in Context.Users) {
|
||||
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
||||
|
||||
if(whoUser == user)
|
||||
|
@ -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,6 +622,18 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1));
|
||||
|
||||
if(!ChatChannel.CheckName(createChanName)) {
|
||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID));
|
||||
break;
|
||||
}
|
||||
|
||||
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),
|
||||
|
@ -603,18 +641,15 @@ namespace SharpChat {
|
|||
Owner = user,
|
||||
};
|
||||
|
||||
try {
|
||||
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));
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
Context.Events.Remove(delMsg);
|
||||
Context.Events.RemoveEvent(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,7 +756,8 @@ namespace SharpChat {
|
|||
int banReasonIndex = 2;
|
||||
ChatUser banUser = null;
|
||||
|
||||
if(banUserTarget == null || (banUser = Context.Users.Get(banUserTarget)) == null) {
|
||||
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;
|
||||
}
|
||||
|
@ -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,10 +895,12 @@ 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]));
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -892,10 +940,12 @@ 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]));
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -920,9 +970,12 @@ 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]));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue