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.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
@ -24,7 +25,6 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasUser(ChatUser user) {
|
public bool HasUser(ChatUser user) {
|
||||||
lock(Users)
|
|
||||||
return Users.Contains(user);
|
return Users.Contains(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,14 +35,11 @@ namespace SharpChat {
|
||||||
user.JoinChannel(this);
|
user.JoinChannel(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock(Users) {
|
|
||||||
if(!HasUser(user))
|
if(!HasUser(user))
|
||||||
Users.Add(user);
|
Users.Add(user);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void UserLeave(ChatUser user) {
|
public void UserLeave(ChatUser user) {
|
||||||
lock(Users)
|
|
||||||
Users.Remove(user);
|
Users.Remove(user);
|
||||||
|
|
||||||
if(user.InChannel(this))
|
if(user.InChannel(this))
|
||||||
|
@ -50,14 +47,11 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Send(IServerPacket packet) {
|
public void Send(IServerPacket packet) {
|
||||||
lock(Users) {
|
|
||||||
foreach(ChatUser user in Users)
|
foreach(ChatUser user in Users)
|
||||||
user.Send(packet);
|
user.Send(packet);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<ChatUser> GetUsers(IEnumerable<ChatUser> exclude = null) {
|
public IEnumerable<ChatUser> GetUsers(IEnumerable<ChatUser> exclude = null) {
|
||||||
lock(Users) {
|
|
||||||
IEnumerable<ChatUser> users = Users.OrderByDescending(x => x.Rank);
|
IEnumerable<ChatUser> users = Users.OrderByDescending(x => x.Rank);
|
||||||
|
|
||||||
if(exclude != null)
|
if(exclude != null)
|
||||||
|
@ -65,7 +59,6 @@ namespace SharpChat {
|
||||||
|
|
||||||
return users.ToList();
|
return users.ToList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public string Pack() {
|
public string Pack() {
|
||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
|
@ -78,5 +71,21 @@ namespace SharpChat {
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool NameEquals(string name) {
|
||||||
|
return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return Name.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CheckName(string name) {
|
||||||
|
return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CheckNameChar(char c) {
|
||||||
|
return char.IsLetter(c) || char.IsNumber(c) || c == '-';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,39 @@
|
||||||
using SharpChat.Events;
|
using SharpChat.Events;
|
||||||
|
using SharpChat.EventStorage;
|
||||||
using SharpChat.Packet;
|
using SharpChat.Packet;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat {
|
||||||
public class ChatContext : IDisposable {
|
public class ChatContext {
|
||||||
public bool IsDisposed { get; private set; }
|
public HashSet<ChatChannel> Channels { get; } = new();
|
||||||
|
public readonly object ChannelsAccess = new();
|
||||||
|
|
||||||
public ChannelManager Channels { get; }
|
public HashSet<ChatUser> Users { get; } = new();
|
||||||
public UserManager Users { get; }
|
public readonly object UsersAccess = new();
|
||||||
public ChatEventManager Events { get; }
|
|
||||||
|
|
||||||
public ChatContext() {
|
public IEventStorage Events { get; }
|
||||||
Users = new(this);
|
public readonly object EventsAccess = new();
|
||||||
Channels = new(this);
|
|
||||||
Events = new(this);
|
public ChatContext(IEventStorage evtStore) {
|
||||||
|
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update() {
|
public void Update() {
|
||||||
CheckPings();
|
lock(UsersAccess)
|
||||||
|
foreach(ChatUser user in Users) {
|
||||||
|
IEnumerable<ChatUserSession> timedOut = user.GetDeadSessions();
|
||||||
|
|
||||||
|
foreach(ChatUserSession sess in timedOut) {
|
||||||
|
user.RemoveSession(sess);
|
||||||
|
sess.Dispose();
|
||||||
|
Logger.Write($"Nuked session {sess.Id} from {user.Username} (timeout)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!user.HasSessions)
|
||||||
|
UserLeave(null, user, UserDisconnectReason.TimeOut);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
|
public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
|
||||||
|
@ -32,27 +47,28 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess, int maxMsgLength) {
|
public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess, int maxMsgLength) {
|
||||||
|
lock(EventsAccess) {
|
||||||
if(!chan.HasUser(user)) {
|
if(!chan.HasUser(user)) {
|
||||||
chan.Send(new UserConnectPacket(DateTimeOffset.Now, 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 AuthSuccessPacket(user, chan, sess, maxMsgLength));
|
||||||
sess.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
|
sess.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
|
||||||
|
|
||||||
IEnumerable<IChatEvent> msgs = Events.GetTargetLog(chan);
|
foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name))
|
||||||
|
|
||||||
foreach(IChatEvent msg in msgs)
|
|
||||||
sess.Send(new ContextMessagePacket(msg));
|
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))
|
if(!chan.HasUser(user))
|
||||||
chan.UserJoin(user);
|
chan.UserJoin(user);
|
||||||
|
|
||||||
if(!Users.Contains(user))
|
lock(UsersAccess)
|
||||||
Users.Add(user);
|
Users.Add(user);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void UserLeave(ChatChannel chan, ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
|
public void UserLeave(ChatChannel chan, ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
|
||||||
user.Status = ChatUserStatus.Offline;
|
user.Status = ChatUserStatus.Offline;
|
||||||
|
@ -65,11 +81,14 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(chan.IsTemporary && chan.Owner == user)
|
if(chan.IsTemporary && chan.Owner == user)
|
||||||
Channels.Remove(chan);
|
lock(ChannelsAccess)
|
||||||
|
RemoveChannel(chan);
|
||||||
|
|
||||||
|
lock(EventsAccess) {
|
||||||
chan.UserLeave(user);
|
chan.UserLeave(user);
|
||||||
chan.Send(new UserDisconnectPacket(DateTimeOffset.Now, user, reason));
|
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) {
|
public void SwitchChannel(ChatUser user, ChatChannel chan, string password) {
|
||||||
|
@ -97,70 +116,96 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ForceChannelSwitch(ChatUser user, ChatChannel chan) {
|
public void ForceChannelSwitch(ChatUser user, ChatChannel chan) {
|
||||||
|
lock(ChannelsAccess)
|
||||||
if(!Channels.Contains(chan))
|
if(!Channels.Contains(chan))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ChatChannel oldChan = user.CurrentChannel;
|
ChatChannel oldChan = user.CurrentChannel;
|
||||||
|
|
||||||
|
lock(EventsAccess) {
|
||||||
oldChan.Send(new UserChannelLeavePacket(user));
|
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));
|
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 ContextClearPacket(chan, ContextClearMode.MessagesUsers));
|
||||||
user.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
|
user.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
|
||||||
|
|
||||||
IEnumerable<IChatEvent> msgs = Events.GetTargetLog(chan);
|
foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name))
|
||||||
|
|
||||||
foreach(IChatEvent msg in msgs)
|
|
||||||
user.Send(new ContextMessagePacket(msg));
|
user.Send(new ContextMessagePacket(msg));
|
||||||
|
|
||||||
user.ForceChannel(chan);
|
user.ForceChannel(chan);
|
||||||
oldChan.UserLeave(user);
|
oldChan.UserLeave(user);
|
||||||
chan.UserJoin(user);
|
chan.UserJoin(user);
|
||||||
|
}
|
||||||
|
|
||||||
if(oldChan.IsTemporary && oldChan.Owner == user)
|
if(oldChan.IsTemporary && oldChan.Owner == user)
|
||||||
Channels.Remove(oldChan);
|
lock(ChannelsAccess)
|
||||||
}
|
RemoveChannel(oldChan);
|
||||||
|
|
||||||
public void CheckPings() {
|
|
||||||
lock(Users)
|
|
||||||
foreach(ChatUser user in Users.All()) {
|
|
||||||
IEnumerable<ChatUserSession> timedOut = user.GetDeadSessions();
|
|
||||||
|
|
||||||
foreach(ChatUserSession sess in timedOut) {
|
|
||||||
user.RemoveSession(sess);
|
|
||||||
sess.Dispose();
|
|
||||||
Logger.Write($"Nuked session {sess.Id} from {user.Username} (timeout)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!user.HasSessions)
|
|
||||||
UserLeave(null, user, UserDisconnectReason.TimeOut);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Send(IServerPacket packet) {
|
public void Send(IServerPacket packet) {
|
||||||
foreach(ChatUser user in Users.All())
|
lock(UsersAccess)
|
||||||
|
foreach(ChatUser user in Users)
|
||||||
user.Send(packet);
|
user.Send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
~ChatContext() {
|
public void UpdateChannel(ChatChannel channel, string name = null, bool? temporary = null, int? hierarchy = null, string password = null) {
|
||||||
DoDispose();
|
if(channel == null)
|
||||||
|
throw new ArgumentNullException(nameof(channel));
|
||||||
|
if(!Channels.Contains(channel))
|
||||||
|
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
|
||||||
|
|
||||||
|
string prevName = channel.Name;
|
||||||
|
int prevHierarchy = channel.Rank;
|
||||||
|
bool nameUpdated = !string.IsNullOrWhiteSpace(name) && name != prevName;
|
||||||
|
|
||||||
|
if(nameUpdated) {
|
||||||
|
if(!ChatChannel.CheckName(name))
|
||||||
|
throw new ArgumentException("Name contains invalid characters.", nameof(name));
|
||||||
|
|
||||||
|
channel.Name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
if(temporary.HasValue)
|
||||||
DoDispose();
|
channel.IsTemporary = temporary.Value;
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
|
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() {
|
public void RemoveChannel(ChatChannel channel) {
|
||||||
if(IsDisposed)
|
if(channel == null || !Channels.Any())
|
||||||
return;
|
return;
|
||||||
IsDisposed = true;
|
|
||||||
|
|
||||||
Events?.Dispose();
|
ChatChannel defaultChannel = Channels.FirstOrDefault();
|
||||||
Channels?.Dispose();
|
if(defaultChannel == null)
|
||||||
Users?.Dispose();
|
return;
|
||||||
|
|
||||||
|
// Remove channel from the listing
|
||||||
|
Channels.Remove(channel);
|
||||||
|
|
||||||
|
// Move all users back to the main channel
|
||||||
|
// TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel.
|
||||||
|
foreach(ChatUser user in channel.GetUsers())
|
||||||
|
SwitchChannel(user, defaultChannel, string.Empty);
|
||||||
|
|
||||||
|
// Broadcast deletion of channel
|
||||||
|
lock(UsersAccess)
|
||||||
|
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank))
|
||||||
|
user.Send(new ChannelDeletePacket(channel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
public ChatRateLimitState State {
|
||||||
get {
|
get {
|
||||||
lock(TimePoints) {
|
|
||||||
if(TimePoints.Count == FLOOD_PROTECTION_AMOUNT) {
|
if(TimePoints.Count == FLOOD_PROTECTION_AMOUNT) {
|
||||||
if((TimePoints.Last() - TimePoints.First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD)
|
if((TimePoints.Last() - TimePoints.First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD)
|
||||||
return ChatRateLimitState.Kick;
|
return ChatRateLimitState.Kick;
|
||||||
|
@ -29,13 +28,11 @@ namespace SharpChat {
|
||||||
return ChatRateLimitState.None;
|
return ChatRateLimitState.None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTimePoint(DateTimeOffset? dto = null) {
|
public void AddTimePoint(DateTimeOffset? dto = null) {
|
||||||
if(!dto.HasValue)
|
if(!dto.HasValue)
|
||||||
dto = DateTimeOffset.Now;
|
dto = DateTimeOffset.Now;
|
||||||
|
|
||||||
lock(TimePoints) {
|
|
||||||
if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT)
|
if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT)
|
||||||
TimePoints.Dequeue();
|
TimePoints.Dequeue();
|
||||||
|
|
||||||
|
@ -43,4 +40,3 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using SharpChat.Packet;
|
using SharpChat.Packet;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -23,10 +22,6 @@ namespace SharpChat {
|
||||||
public bool HasFloodProtection
|
public bool HasFloodProtection
|
||||||
=> Rank < RANK_NO_FLOOD;
|
=> Rank < RANK_NO_FLOOD;
|
||||||
|
|
||||||
public bool Equals([AllowNull] BasicUser other) {
|
|
||||||
return UserId == other.UserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DisplayName {
|
public string DisplayName {
|
||||||
get {
|
get {
|
||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
|
@ -71,6 +66,18 @@ namespace SharpChat {
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return UserId.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return Equals(obj as BasicUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(BasicUser other) {
|
||||||
|
return UserId == other?.UserId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChatUser : BasicUser, IPacketTarget {
|
public class ChatUser : BasicUser, IPacketTarget {
|
||||||
|
@ -83,12 +90,7 @@ namespace SharpChat {
|
||||||
|
|
||||||
public string TargetName => "@log";
|
public string TargetName => "@log";
|
||||||
|
|
||||||
public ChatChannel Channel {
|
public ChatChannel Channel => Channels.FirstOrDefault();
|
||||||
get {
|
|
||||||
lock(Channels)
|
|
||||||
return Channels.FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This needs to be a session thing
|
// This needs to be a session thing
|
||||||
public ChatChannel CurrentChannel { get; private set; }
|
public ChatChannel CurrentChannel { get; private set; }
|
||||||
|
@ -96,26 +98,11 @@ namespace SharpChat {
|
||||||
public bool IsSilenced
|
public bool IsSilenced
|
||||||
=> DateTimeOffset.UtcNow - SilencedUntil <= TimeSpan.Zero;
|
=> DateTimeOffset.UtcNow - SilencedUntil <= TimeSpan.Zero;
|
||||||
|
|
||||||
public bool HasSessions {
|
public bool HasSessions => Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Any();
|
||||||
get {
|
|
||||||
lock(Sessions)
|
|
||||||
return Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Any();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int SessionCount {
|
public int SessionCount => Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Count();
|
||||||
get {
|
|
||||||
lock(Sessions)
|
|
||||||
return Sessions.Where(c => !c.HasTimedOut && !c.IsDisposed).Count();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IPAddress> RemoteAddresses {
|
public IEnumerable<IPAddress> RemoteAddresses => Sessions.Select(c => c.RemoteAddress);
|
||||||
get {
|
|
||||||
lock(Sessions)
|
|
||||||
return Sessions.Select(c => c.RemoteAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatUser() {
|
public ChatUser() {
|
||||||
}
|
}
|
||||||
|
@ -140,53 +127,42 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Send(IServerPacket packet) {
|
public void Send(IServerPacket packet) {
|
||||||
lock(Sessions)
|
|
||||||
foreach(ChatUserSession conn in Sessions)
|
foreach(ChatUserSession conn in Sessions)
|
||||||
conn.Send(packet);
|
conn.Send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close() {
|
public void Close() {
|
||||||
lock(Sessions) {
|
|
||||||
foreach(ChatUserSession conn in Sessions)
|
foreach(ChatUserSession conn in Sessions)
|
||||||
conn.Dispose();
|
conn.Dispose();
|
||||||
Sessions.Clear();
|
Sessions.Clear();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void ForceChannel(ChatChannel chan = null) {
|
public void ForceChannel(ChatChannel chan = null) {
|
||||||
Send(new UserChannelForceJoinPacket(chan ?? CurrentChannel));
|
Send(new UserChannelForceJoinPacket(chan ?? CurrentChannel));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FocusChannel(ChatChannel chan) {
|
public void FocusChannel(ChatChannel chan) {
|
||||||
lock(Channels) {
|
|
||||||
if(InChannel(chan))
|
if(InChannel(chan))
|
||||||
CurrentChannel = chan;
|
CurrentChannel = chan;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public bool InChannel(ChatChannel chan) {
|
public bool InChannel(ChatChannel chan) {
|
||||||
lock(Channels)
|
|
||||||
return Channels.Contains(chan);
|
return Channels.Contains(chan);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void JoinChannel(ChatChannel chan) {
|
public void JoinChannel(ChatChannel chan) {
|
||||||
lock(Channels) {
|
|
||||||
if(!InChannel(chan)) {
|
if(!InChannel(chan)) {
|
||||||
Channels.Add(chan);
|
Channels.Add(chan);
|
||||||
CurrentChannel = chan;
|
CurrentChannel = chan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void LeaveChannel(ChatChannel chan) {
|
public void LeaveChannel(ChatChannel chan) {
|
||||||
lock(Channels) {
|
|
||||||
Channels.Remove(chan);
|
Channels.Remove(chan);
|
||||||
CurrentChannel = Channels.FirstOrDefault();
|
CurrentChannel = Channels.FirstOrDefault();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<ChatChannel> GetChannels() {
|
public IEnumerable<ChatChannel> GetChannels() {
|
||||||
lock(Channels)
|
|
||||||
return Channels.ToList();
|
return Channels.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +171,6 @@ namespace SharpChat {
|
||||||
return;
|
return;
|
||||||
sess.User = this;
|
sess.User = this;
|
||||||
|
|
||||||
lock(Sessions)
|
|
||||||
Sessions.Add(sess);
|
Sessions.Add(sess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,13 +180,17 @@ namespace SharpChat {
|
||||||
if(!sess.IsDisposed) // this could be possible
|
if(!sess.IsDisposed) // this could be possible
|
||||||
sess.User = null;
|
sess.User = null;
|
||||||
|
|
||||||
lock(Sessions)
|
|
||||||
Sessions.Remove(sess);
|
Sessions.Remove(sess);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ChatUserSession> GetDeadSessions() {
|
public IEnumerable<ChatUserSession> GetDeadSessions() {
|
||||||
lock(Sessions)
|
|
||||||
return Sessions.Where(x => x.HasTimedOut || x.IsDisposed).ToList();
|
return Sessions.Where(x => x.HasTimedOut || x.IsDisposed).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool NameEquals(string name) {
|
||||||
|
return string.Equals(name, Username, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| string.Equals(name, Nickname, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| string.Equals(name, DisplayName, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,14 @@ namespace SharpChat.Config {
|
||||||
private string Name { get; }
|
private string Name { get; }
|
||||||
private TimeSpan Lifetime { get; }
|
private TimeSpan Lifetime { get; }
|
||||||
private T Fallback { get; }
|
private T Fallback { get; }
|
||||||
private object Sync { get; } = new();
|
private object ConfigAccess { get; } = new();
|
||||||
|
|
||||||
private object CurrentValue { get; set; }
|
private object CurrentValue { get; set; }
|
||||||
private DateTimeOffset LastRead { get; set; }
|
private DateTimeOffset LastRead { get; set; }
|
||||||
|
|
||||||
public T Value {
|
public T Value {
|
||||||
get {
|
get {
|
||||||
lock(Sync) {
|
lock(ConfigAccess) { // this lock doesn't really make sense since it doesn't affect other config calls
|
||||||
DateTimeOffset now = DateTimeOffset.Now;
|
DateTimeOffset now = DateTimeOffset.Now;
|
||||||
if((now - LastRead) >= Lifetime) {
|
if((now - LastRead) >= Lifetime) {
|
||||||
LastRead = now;
|
LastRead = now;
|
||||||
|
@ -37,10 +37,8 @@ namespace SharpChat.Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Refresh() {
|
public void Refresh() {
|
||||||
lock(Sync) {
|
|
||||||
LastRead = DateTimeOffset.MinValue;
|
LastRead = DateTimeOffset.MinValue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() {
|
public override string ToString() {
|
||||||
return Value.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 MySqlConnector;
|
||||||
using SharpChat.Config;
|
|
||||||
using SharpChat.Events;
|
using SharpChat.Events;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat.EventStorage {
|
||||||
public static partial class Database {
|
public partial class MariaDBEventStorage : IEventStorage {
|
||||||
private static string ConnectionString = null;
|
private string ConnectionString { get; }
|
||||||
|
|
||||||
public static bool HasDatabase
|
public MariaDBEventStorage(string connString) {
|
||||||
=> !string.IsNullOrWhiteSpace(ConnectionString);
|
ConnectionString = connString ?? throw new ArgumentNullException(nameof(connString));
|
||||||
|
|
||||||
public static void Init(IConfig config) {
|
|
||||||
Init(
|
|
||||||
config.ReadValue("host", "localhost"),
|
|
||||||
config.ReadValue("user", string.Empty),
|
|
||||||
config.ReadValue("pass", string.Empty),
|
|
||||||
config.ReadValue("db", "sharpchat")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Init(string host, string username, string password, string database) {
|
public void AddEvent(IChatEvent evt) {
|
||||||
ConnectionString = new MySqlConnectionStringBuilder {
|
if(evt == null)
|
||||||
Server = host,
|
throw new ArgumentNullException(nameof(evt));
|
||||||
UserID = username,
|
|
||||||
Password = password,
|
|
||||||
Database = database,
|
|
||||||
OldGuids = false,
|
|
||||||
TreatTinyAsBoolean = false,
|
|
||||||
CharacterSet = "utf8mb4",
|
|
||||||
SslMode = MySqlSslMode.None,
|
|
||||||
ForceSynchronous = true,
|
|
||||||
ConnectionTimeout = 5,
|
|
||||||
}.ToString();
|
|
||||||
RunMigrations();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Deinit() {
|
|
||||||
ConnectionString = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MySqlConnection GetConnection() {
|
|
||||||
if(!HasDatabase)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
MySqlConnection conn = new(ConnectionString);
|
|
||||||
conn.Open();
|
|
||||||
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int RunCommand(string command, params MySqlParameter[] parameters) {
|
|
||||||
if(!HasDatabase)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
using MySqlConnection conn = GetConnection();
|
|
||||||
using MySqlCommand cmd = conn.CreateCommand();
|
|
||||||
if(parameters?.Length > 0)
|
|
||||||
cmd.Parameters.AddRange(parameters);
|
|
||||||
cmd.CommandText = command;
|
|
||||||
return cmd.ExecuteNonQuery();
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MySqlDataReader RunQuery(string command, params MySqlParameter[] parameters) {
|
|
||||||
if(!HasDatabase)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
MySqlConnection conn = GetConnection();
|
|
||||||
MySqlCommand cmd = conn.CreateCommand();
|
|
||||||
if(parameters?.Length > 0)
|
|
||||||
cmd.Parameters.AddRange(parameters);
|
|
||||||
cmd.CommandText = command;
|
|
||||||
return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object RunQueryValue(string command, params MySqlParameter[] parameters) {
|
|
||||||
if(!HasDatabase)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
using MySqlConnection conn = GetConnection();
|
|
||||||
using MySqlCommand cmd = conn.CreateCommand();
|
|
||||||
if(parameters?.Length > 0)
|
|
||||||
cmd.Parameters.AddRange(parameters);
|
|
||||||
cmd.CommandText = command;
|
|
||||||
cmd.Prepare();
|
|
||||||
return cmd.ExecuteScalar();
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LogEvent(IChatEvent evt) {
|
|
||||||
if(evt.SequenceId < 1)
|
if(evt.SequenceId < 1)
|
||||||
evt.SequenceId = SharpId.Next();
|
evt.SequenceId = SharpId.Next();
|
||||||
|
|
||||||
|
@ -131,67 +40,7 @@ namespace SharpChat {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteEvent(IChatEvent evt) {
|
public IChatEvent GetEvent(long seqId) {
|
||||||
RunCommand(
|
|
||||||
"UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL",
|
|
||||||
new MySqlParameter("id", evt.SequenceId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IChatEvent ReadEvent(MySqlDataReader reader, IPacketTarget target = null) {
|
|
||||||
Type evtType = Type.GetType(Encoding.ASCII.GetString((byte[])reader["event_type"]));
|
|
||||||
IChatEvent evt = JsonSerializer.Deserialize(Encoding.ASCII.GetString((byte[])reader["event_data"]), evtType) as IChatEvent;
|
|
||||||
evt.SequenceId = reader.GetInt64("event_id");
|
|
||||||
evt.Target = target;
|
|
||||||
evt.TargetName = target?.TargetName ?? Encoding.ASCII.GetString((byte[])reader["event_target"]);
|
|
||||||
evt.Flags = (ChatMessageFlags)reader.GetByte("event_flags");
|
|
||||||
evt.DateTime = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created"));
|
|
||||||
|
|
||||||
if(!reader.IsDBNull(reader.GetOrdinal("event_sender"))) {
|
|
||||||
evt.Sender = new BasicUser {
|
|
||||||
UserId = reader.GetInt64("event_sender"),
|
|
||||||
Username = reader.GetString("event_sender_name"),
|
|
||||||
Colour = ChatColour.FromMisuzu(reader.GetInt32("event_sender_colour")),
|
|
||||||
Rank = reader.GetInt32("event_sender_rank"),
|
|
||||||
Nickname = reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? null : reader.GetString("event_sender_nick"),
|
|
||||||
Permissions = (ChatUserPermissions)reader.GetInt32("event_sender_perms")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return evt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<IChatEvent> GetEvents(IPacketTarget target, int amount, int offset) {
|
|
||||||
List<IChatEvent> events = new();
|
|
||||||
|
|
||||||
try {
|
|
||||||
using MySqlDataReader reader = RunQuery(
|
|
||||||
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`"
|
|
||||||
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
|
|
||||||
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
|
|
||||||
+ " FROM `sqc_events`"
|
|
||||||
+ " WHERE `event_deleted` IS NULL AND `event_target` = @target"
|
|
||||||
+ " AND `event_id` > @offset"
|
|
||||||
+ " ORDER BY `event_id` DESC"
|
|
||||||
+ " LIMIT @amount",
|
|
||||||
new MySqlParameter("target", target.TargetName),
|
|
||||||
new MySqlParameter("amount", amount),
|
|
||||||
new MySqlParameter("offset", offset)
|
|
||||||
);
|
|
||||||
|
|
||||||
while(reader.Read()) {
|
|
||||||
IChatEvent evt = ReadEvent(reader, target);
|
|
||||||
if(evt != null)
|
|
||||||
events.Add(evt);
|
|
||||||
}
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IChatEvent GetEvent(long seqId) {
|
|
||||||
try {
|
try {
|
||||||
using MySqlDataReader reader = RunQuery(
|
using MySqlDataReader reader = RunQuery(
|
||||||
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
|
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
|
||||||
|
@ -213,5 +62,66 @@ namespace SharpChat {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IChatEvent ReadEvent(MySqlDataReader reader) {
|
||||||
|
Type evtType = Type.GetType(Encoding.ASCII.GetString((byte[])reader["event_type"]));
|
||||||
|
IChatEvent evt = JsonSerializer.Deserialize(Encoding.ASCII.GetString((byte[])reader["event_data"]), evtType) as IChatEvent;
|
||||||
|
evt.SequenceId = reader.GetInt64("event_id");
|
||||||
|
evt.TargetName = Encoding.ASCII.GetString((byte[])reader["event_target"]);
|
||||||
|
evt.Flags = (ChatMessageFlags)reader.GetByte("event_flags");
|
||||||
|
evt.DateTime = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created"));
|
||||||
|
|
||||||
|
if(!reader.IsDBNull(reader.GetOrdinal("event_sender"))) {
|
||||||
|
evt.Sender = new BasicUser {
|
||||||
|
UserId = reader.GetInt64("event_sender"),
|
||||||
|
Username = reader.GetString("event_sender_name"),
|
||||||
|
Colour = ChatColour.FromMisuzu(reader.GetInt32("event_sender_colour")),
|
||||||
|
Rank = reader.GetInt32("event_sender_rank"),
|
||||||
|
Nickname = reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? null : reader.GetString("event_sender_nick"),
|
||||||
|
Permissions = (ChatUserPermissions)reader.GetInt32("event_sender_perms")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IChatEvent> GetTargetEventLog(string target, int amount = 20, int offset = 0) {
|
||||||
|
List<IChatEvent> events = new();
|
||||||
|
|
||||||
|
try {
|
||||||
|
using MySqlDataReader reader = RunQuery(
|
||||||
|
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
|
||||||
|
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
|
||||||
|
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
|
||||||
|
+ " FROM `sqc_events`"
|
||||||
|
+ " WHERE `event_deleted` IS NULL AND `event_target` = @target"
|
||||||
|
+ " AND `event_id` > @offset"
|
||||||
|
+ " ORDER BY `event_id` DESC"
|
||||||
|
+ " LIMIT @amount",
|
||||||
|
new MySqlParameter("target", target),
|
||||||
|
new MySqlParameter("amount", amount),
|
||||||
|
new MySqlParameter("offset", offset)
|
||||||
|
);
|
||||||
|
|
||||||
|
while(reader.Read()) {
|
||||||
|
IChatEvent evt = ReadEvent(reader);
|
||||||
|
if(evt != null)
|
||||||
|
events.Add(evt);
|
||||||
|
}
|
||||||
|
} catch(MySqlException ex) {
|
||||||
|
Logger.Write(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveEvent(IChatEvent evt) {
|
||||||
|
if(evt == null)
|
||||||
|
throw new ArgumentNullException(nameof(evt));
|
||||||
|
RunCommand(
|
||||||
|
"UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL",
|
||||||
|
new MySqlParameter("id", evt.SequenceId)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
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 MySqlConnector;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat.EventStorage {
|
||||||
public static partial class Database {
|
public partial class MariaDBEventStorage {
|
||||||
private static void DoMigration(string name, Action action) {
|
private void DoMigration(string name, Action action) {
|
||||||
bool done = (long)RunQueryValue(
|
bool done = (long)RunQueryValue(
|
||||||
"SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name",
|
"SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name",
|
||||||
new MySqlParameter("name", name)
|
new MySqlParameter("name", name)
|
||||||
|
@ -18,7 +18,7 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunMigrations() {
|
public void RunMigrations() {
|
||||||
RunCommand(
|
RunCommand(
|
||||||
"CREATE TABLE IF NOT EXISTS `sqc_migrations` ("
|
"CREATE TABLE IF NOT EXISTS `sqc_migrations` ("
|
||||||
+ "`migration_name` VARCHAR(255) NOT NULL,"
|
+ "`migration_name` VARCHAR(255) NOT NULL,"
|
||||||
|
@ -31,7 +31,7 @@ namespace SharpChat {
|
||||||
DoMigration("create_events_table", CreateEventsTable);
|
DoMigration("create_events_table", CreateEventsTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateEventsTable() {
|
private void CreateEventsTable() {
|
||||||
RunCommand(
|
RunCommand(
|
||||||
"CREATE TABLE `sqc_events` ("
|
"CREATE TABLE `sqc_events` ("
|
||||||
+ "`event_id` BIGINT(20) NOT NULL,"
|
+ "`event_id` BIGINT(20) NOT NULL,"
|
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.Config;
|
||||||
|
using SharpChat.EventStorage;
|
||||||
using SharpChat.Misuzu;
|
using SharpChat.Misuzu;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -46,8 +47,6 @@ namespace SharpChat {
|
||||||
|
|
||||||
using IConfig config = new StreamConfig(configFile);
|
using IConfig config = new StreamConfig(configFile);
|
||||||
|
|
||||||
Database.Init(config.ScopeTo("mariadb"));
|
|
||||||
|
|
||||||
if(hasCancelled) return;
|
if(hasCancelled) return;
|
||||||
|
|
||||||
using HttpClient httpClient = new(new HttpClientHandler() {
|
using HttpClient httpClient = new(new HttpClientHandler() {
|
||||||
|
@ -61,7 +60,18 @@ namespace SharpChat {
|
||||||
|
|
||||||
if(hasCancelled) return;
|
if(hasCancelled) return;
|
||||||
|
|
||||||
using SockChatServer scs = new(httpClient, msz, config.ScopeTo("chat"));
|
IEventStorage evtStore;
|
||||||
|
if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) {
|
||||||
|
evtStore = new VirtualEventStorage();
|
||||||
|
} else {
|
||||||
|
MariaDBEventStorage mdbes = new(MariaDBEventStorage.BuildConnString(config.ScopeTo("mariadb")));
|
||||||
|
evtStore = mdbes;
|
||||||
|
mdbes.RunMigrations();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasCancelled) return;
|
||||||
|
|
||||||
|
using SockChatServer scs = new(httpClient, msz, evtStore, config.ScopeTo("chat"));
|
||||||
scs.Listen(mre);
|
scs.Listen(mre);
|
||||||
|
|
||||||
mre.WaitOne();
|
mre.WaitOne();
|
||||||
|
|
|
@ -3,27 +3,22 @@ using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat {
|
||||||
public static class RNG {
|
public static class RNG {
|
||||||
private static object Lock { get; } = new();
|
|
||||||
private static Random NormalRandom { get; } = new();
|
private static Random NormalRandom { get; } = new();
|
||||||
private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create();
|
private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create();
|
||||||
|
|
||||||
public static int Next() {
|
public static int Next() {
|
||||||
lock(Lock)
|
|
||||||
return NormalRandom.Next();
|
return NormalRandom.Next();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int Next(int max) {
|
public static int Next(int max) {
|
||||||
lock(Lock)
|
|
||||||
return NormalRandom.Next(max);
|
return NormalRandom.Next(max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int Next(int min, int max) {
|
public static int Next(int min, int max) {
|
||||||
lock(Lock)
|
|
||||||
return NormalRandom.Next(min, max);
|
return NormalRandom.Next(min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void NextBytes(byte[] buffer) {
|
public static void NextBytes(byte[] buffer) {
|
||||||
lock(Lock)
|
|
||||||
SecureRandom.GetBytes(buffer);
|
SecureRandom.GetBytes(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using SharpChat.Commands;
|
using SharpChat.Commands;
|
||||||
using SharpChat.Config;
|
using SharpChat.Config;
|
||||||
using SharpChat.Events;
|
using SharpChat.Events;
|
||||||
|
using SharpChat.EventStorage;
|
||||||
using SharpChat.Misuzu;
|
using SharpChat.Misuzu;
|
||||||
using SharpChat.Packet;
|
using SharpChat.Packet;
|
||||||
using System;
|
using System;
|
||||||
|
@ -45,17 +46,19 @@ namespace SharpChat {
|
||||||
};
|
};
|
||||||
|
|
||||||
public List<ChatUserSession> Sessions { get; } = new List<ChatUserSession>();
|
public List<ChatUserSession> Sessions { get; } = new List<ChatUserSession>();
|
||||||
private object SessionsLock { get; } = new object();
|
private object SessionsAccess { get; } = new object();
|
||||||
|
|
||||||
public ChatUserSession GetSession(IWebSocketConnection conn) {
|
public ChatUserSession GetSession(IWebSocketConnection conn) {
|
||||||
lock(SessionsLock)
|
lock(SessionsAccess)
|
||||||
return Sessions.FirstOrDefault(x => x.Connection == conn);
|
return Sessions.FirstOrDefault(x => x.Connection == conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ManualResetEvent Shutdown { get; set; }
|
private ManualResetEvent Shutdown { get; set; }
|
||||||
private bool IsShuttingDown = false;
|
private bool IsShuttingDown = false;
|
||||||
|
|
||||||
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IConfig config) {
|
private ChatChannel DefaultChannel { get; set; }
|
||||||
|
|
||||||
|
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IEventStorage evtStore, IConfig config) {
|
||||||
Logger.Write("Initialising Sock Chat server...");
|
Logger.Write("Initialising Sock Chat server...");
|
||||||
|
|
||||||
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
@ -65,7 +68,7 @@ namespace SharpChat {
|
||||||
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
|
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
|
||||||
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
|
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
|
||||||
|
|
||||||
Context = new ChatContext();
|
Context = new ChatContext(evtStore);
|
||||||
|
|
||||||
string[] channelNames = config.ReadValue("channels", new[] { "lounge" });
|
string[] channelNames = config.ReadValue("channels", new[] { "lounge" });
|
||||||
|
|
||||||
|
@ -82,6 +85,8 @@ namespace SharpChat {
|
||||||
channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0);
|
channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0);
|
||||||
|
|
||||||
Context.Channels.Add(channelInfo);
|
Context.Channels.Add(channelInfo);
|
||||||
|
|
||||||
|
DefaultChannel ??= channelInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
|
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
|
||||||
|
@ -107,7 +112,9 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOpen(IWebSocketConnection conn) {
|
private void OnOpen(IWebSocketConnection conn) {
|
||||||
lock(SessionsLock) {
|
Logger.Write($"Connection opened from {conn.ConnectionInfo.ClientIpAddress}:{conn.ConnectionInfo.ClientPort}");
|
||||||
|
|
||||||
|
lock(SessionsAccess) {
|
||||||
if(!Sessions.Any(x => x.Connection == conn))
|
if(!Sessions.Any(x => x.Connection == conn))
|
||||||
Sessions.Add(new ChatUserSession(conn));
|
Sessions.Add(new ChatUserSession(conn));
|
||||||
}
|
}
|
||||||
|
@ -116,6 +123,8 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClose(IWebSocketConnection conn) {
|
private void OnClose(IWebSocketConnection conn) {
|
||||||
|
Logger.Write($"Connection closed from {conn.ConnectionInfo.ClientIpAddress}:{conn.ConnectionInfo.ClientPort}");
|
||||||
|
|
||||||
ChatUserSession sess = GetSession(conn);
|
ChatUserSession sess = GetSession(conn);
|
||||||
|
|
||||||
// Remove connection from user
|
// Remove connection from user
|
||||||
|
@ -133,7 +142,7 @@ namespace SharpChat {
|
||||||
Context.Update();
|
Context.Update();
|
||||||
|
|
||||||
// Remove connection from server
|
// Remove connection from server
|
||||||
lock(SessionsLock)
|
lock(SessionsAccess)
|
||||||
Sessions.Remove(sess);
|
Sessions.Remove(sess);
|
||||||
|
|
||||||
sess?.Dispose();
|
sess?.Dispose();
|
||||||
|
@ -195,7 +204,9 @@ namespace SharpChat {
|
||||||
|
|
||||||
lock(BumpAccess) {
|
lock(BumpAccess) {
|
||||||
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
|
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)
|
.Where(u => u.HasSessions && u.Status == ChatUserStatus.Online)
|
||||||
.Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty))
|
.Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
@ -279,7 +290,8 @@ namespace SharpChat {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatUser aUser = Context.Users.Get(fai.UserId);
|
lock(Context.UsersAccess) {
|
||||||
|
ChatUser aUser = Context.Users.FirstOrDefault(u => u.UserId == fai.UserId);
|
||||||
|
|
||||||
if(aUser == null)
|
if(aUser == null)
|
||||||
aUser = new ChatUser(fai);
|
aUser = new ChatUser(fai);
|
||||||
|
@ -310,7 +322,8 @@ namespace SharpChat {
|
||||||
sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line));
|
sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line));
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess, MaxMessageLength);
|
Context.HandleJoin(aUser, DefaultChannel, sess, MaxMessageLength);
|
||||||
|
}
|
||||||
}).Wait();
|
}).Wait();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -370,8 +383,10 @@ namespace SharpChat {
|
||||||
Text = messageText,
|
Text = messageText,
|
||||||
};
|
};
|
||||||
|
|
||||||
Context.Events.Add(message);
|
lock(Context.EventsAccess) {
|
||||||
|
Context.Events.AddEvent(message);
|
||||||
mChannel.Send(new ChatMessageAddPacket(message));
|
mChannel.Send(new ChatMessageAddPacket(message));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -408,7 +423,8 @@ namespace SharpChat {
|
||||||
int offset = 1;
|
int offset = 1;
|
||||||
|
|
||||||
if(setOthersNick && parts.Length > 1 && long.TryParse(parts[1], out long targetUserId) && targetUserId > 0) {
|
if(setOthersNick && parts.Length > 1 && long.TryParse(parts[1], out long targetUserId) && targetUserId > 0) {
|
||||||
targetUser = Context.Users.Get(targetUserId);
|
lock(Context.UsersAccess)
|
||||||
|
targetUser = Context.Users.FirstOrDefault(u => u.UserId == targetUserId);
|
||||||
offset = 2;
|
offset = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,7 +450,8 @@ namespace SharpChat {
|
||||||
else if(string.IsNullOrEmpty(nickStr))
|
else if(string.IsNullOrEmpty(nickStr))
|
||||||
nickStr = null;
|
nickStr = null;
|
||||||
|
|
||||||
if(nickStr != null && Context.Users.Get(nickStr) != null) {
|
lock(Context.UsersAccess)
|
||||||
|
if(!string.IsNullOrWhiteSpace(nickStr) && Context.Users.Any(u => u.NameEquals(nickStr))) {
|
||||||
user.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr));
|
user.Send(new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -450,10 +467,13 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatUser whisperUser = Context.Users.Get(parts[1]);
|
ChatUser whisperUser;
|
||||||
|
string whisperUserStr = parts.ElementAtOrDefault(1);
|
||||||
|
lock(Context.UsersAccess)
|
||||||
|
whisperUser = Context.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr));
|
||||||
|
|
||||||
if(whisperUser == null) {
|
if(whisperUser == null) {
|
||||||
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts[1]));
|
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, whisperUserStr));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,7 +519,9 @@ namespace SharpChat {
|
||||||
string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty;
|
string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty;
|
||||||
|
|
||||||
if(!string.IsNullOrEmpty(whoChanStr)) {
|
if(!string.IsNullOrEmpty(whoChanStr)) {
|
||||||
ChatChannel whoChan = Context.Channels.Get(whoChanStr);
|
ChatChannel whoChan;
|
||||||
|
lock(Context.ChannelsAccess)
|
||||||
|
whoChan = Context.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr));
|
||||||
|
|
||||||
if(whoChan == null) {
|
if(whoChan == null) {
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr));
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr));
|
||||||
|
@ -527,7 +549,8 @@ namespace SharpChat {
|
||||||
|
|
||||||
user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
|
user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
|
||||||
} else {
|
} else {
|
||||||
foreach(ChatUser whoUser in Context.Users.All()) {
|
lock(Context.UsersAccess)
|
||||||
|
foreach(ChatUser whoUser in Context.Users) {
|
||||||
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
||||||
|
|
||||||
if(whoUser == user)
|
if(whoUser == user)
|
||||||
|
@ -563,10 +586,13 @@ namespace SharpChat {
|
||||||
if(parts.Length < 2)
|
if(parts.Length < 2)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
ChatChannel joinChan = Context.Channels.Get(parts[1]);
|
string joinChanStr = parts.ElementAtOrDefault(1);
|
||||||
|
ChatChannel joinChan;
|
||||||
|
lock(Context.ChannelsAccess)
|
||||||
|
joinChan = Context.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr));
|
||||||
|
|
||||||
if(joinChan == null) {
|
if(joinChan == null) {
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, parts[1]));
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, joinChanStr));
|
||||||
user.ForceChannel();
|
user.ForceChannel();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -596,6 +622,18 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1));
|
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() {
|
ChatChannel createChan = new() {
|
||||||
Name = createChanName,
|
Name = createChanName,
|
||||||
IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent),
|
IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent),
|
||||||
|
@ -603,18 +641,15 @@ namespace SharpChat {
|
||||||
Owner = user,
|
Owner = user,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
|
||||||
Context.Channels.Add(createChan);
|
Context.Channels.Add(createChan);
|
||||||
} catch(ChannelExistException) {
|
lock(Context.UsersAccess) {
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChan.Name));
|
foreach(ChatUser ccu in Context.Users.Where(u => u.Rank >= channel.Rank))
|
||||||
break;
|
ccu.Send(new ChannelCreatePacket(channel));
|
||||||
} catch(ChannelInvalidNameException) {
|
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NAME_INVALID));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.SwitchChannel(user, createChan, createChan.Password);
|
Context.SwitchChannel(user, createChan, createChan.Password);
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name));
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "delchan": // delete a channel
|
case "delchan": // delete a channel
|
||||||
if(parts.Length < 2 || string.IsNullOrWhiteSpace(parts[1])) {
|
if(parts.Length < 2 || string.IsNullOrWhiteSpace(parts[1])) {
|
||||||
|
@ -623,7 +658,9 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
string delChanName = string.Join('_', parts.Skip(1));
|
string delChanName = string.Join('_', parts.Skip(1));
|
||||||
ChatChannel delChan = Context.Channels.Get(delChanName);
|
ChatChannel delChan;
|
||||||
|
lock(Context.ChannelsAccess)
|
||||||
|
delChan = Context.Channels.FirstOrDefault(c => c.NameEquals(delChanName));
|
||||||
|
|
||||||
if(delChan == null) {
|
if(delChan == null) {
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName));
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName));
|
||||||
|
@ -635,7 +672,8 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.Channels.Remove(delChan);
|
lock(Context.ChannelsAccess)
|
||||||
|
Context.RemoveChannel(delChan);
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name));
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name));
|
||||||
break;
|
break;
|
||||||
case "password": // set a password on the channel
|
case "password": // set a password on the channel
|
||||||
|
@ -650,7 +688,8 @@ namespace SharpChat {
|
||||||
if(string.IsNullOrWhiteSpace(chanPass))
|
if(string.IsNullOrWhiteSpace(chanPass))
|
||||||
chanPass = string.Empty;
|
chanPass = string.Empty;
|
||||||
|
|
||||||
Context.Channels.Update(channel, password: chanPass);
|
lock(Context.ChannelsAccess)
|
||||||
|
Context.UpdateChannel(channel, password: chanPass);
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false));
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false));
|
||||||
break;
|
break;
|
||||||
case "privilege": // sets a minimum hierarchy requirement on the channel
|
case "privilege": // sets a minimum hierarchy requirement on the channel
|
||||||
|
@ -666,7 +705,8 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.Channels.Update(channel, hierarchy: chanHierarchy);
|
lock(Context.ChannelsAccess)
|
||||||
|
Context.UpdateChannel(channel, hierarchy: chanHierarchy);
|
||||||
user.Send(new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false));
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -691,14 +731,16 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
IChatEvent delMsg = Context.Events.Get(delSeqId);
|
lock(Context.EventsAccess) {
|
||||||
|
IChatEvent delMsg = Context.Events.GetEvent(delSeqId);
|
||||||
|
|
||||||
if(delMsg == null || delMsg.Sender.Rank > user.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != user.UserId)) {
|
if(delMsg == null || delMsg.Sender.Rank > user.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != user.UserId)) {
|
||||||
user.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR));
|
user.Send(new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.Events.Remove(delMsg);
|
Context.Events.RemoveEvent(delMsg);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "kick": // kick a user from the server
|
case "kick": // kick a user from the server
|
||||||
case "ban": // ban a user from the server, this differs from /kick in that it adds all remote address to the ip banlist
|
case "ban": // ban a user from the server, this differs from /kick in that it adds all remote address to the ip banlist
|
||||||
|
@ -714,7 +756,8 @@ namespace SharpChat {
|
||||||
int banReasonIndex = 2;
|
int banReasonIndex = 2;
|
||||||
ChatUser banUser = null;
|
ChatUser banUser = null;
|
||||||
|
|
||||||
if(banUserTarget == null || (banUser = Context.Users.Get(banUserTarget)) == null) {
|
lock(Context.UsersAccess)
|
||||||
|
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));
|
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -776,10 +819,13 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatUser unbanUser = Context.Users.Get(unbanUserTarget);
|
ChatUser unbanUser;
|
||||||
|
lock(Context.UsersAccess)
|
||||||
|
unbanUser = Context.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget));
|
||||||
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
|
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
|
||||||
unbanUserTargetIsName = false;
|
unbanUserTargetIsName = false;
|
||||||
unbanUser = Context.Users.Get(unbanUserId);
|
lock(Context.UsersAccess)
|
||||||
|
unbanUser = Context.Users.FirstOrDefault(u => u.UserId == unbanUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(unbanUser != null)
|
if(unbanUser != null)
|
||||||
|
@ -849,10 +895,12 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string silUserStr = parts.ElementAtOrDefault(1);
|
||||||
ChatUser silUser;
|
ChatUser silUser;
|
||||||
|
|
||||||
if(parts.Length < 2 || (silUser = Context.Users.Get(parts[1])) == null) {
|
lock(Context.UsersAccess)
|
||||||
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1]));
|
if(parts.Length < 2 || (silUser = Context.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) {
|
||||||
|
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : silUserStr));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -892,10 +940,12 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string unsilUserStr = parts.ElementAtOrDefault(1);
|
||||||
ChatUser unsilUser;
|
ChatUser unsilUser;
|
||||||
|
|
||||||
if(parts.Length < 2 || (unsilUser = Context.Users.Get(parts[1])) == null) {
|
lock(Context.UsersAccess)
|
||||||
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1]));
|
if(parts.Length < 2 || (unsilUser = Context.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) {
|
||||||
|
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : unsilUserStr));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -920,9 +970,12 @@ namespace SharpChat {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string ipUserStr = parts.ElementAtOrDefault(1);
|
||||||
ChatUser ipUser;
|
ChatUser ipUser;
|
||||||
if(parts.Length < 2 || (ipUser = Context.Users.Get(parts[1])) == null) {
|
|
||||||
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? "User" : parts[1]));
|
lock(Context.UsersAccess)
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -942,7 +995,7 @@ namespace SharpChat {
|
||||||
IsShuttingDown = true;
|
IsShuttingDown = true;
|
||||||
|
|
||||||
if(commandName == "restart")
|
if(commandName == "restart")
|
||||||
lock(SessionsLock)
|
lock(SessionsAccess)
|
||||||
Sessions.ForEach(s => s.PrepareForRestart());
|
Sessions.ForEach(s => s.PrepareForRestart());
|
||||||
|
|
||||||
Context.Update();
|
Context.Update();
|
||||||
|
@ -971,11 +1024,10 @@ namespace SharpChat {
|
||||||
return;
|
return;
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
|
|
||||||
lock(SessionsLock)
|
lock(SessionsAccess)
|
||||||
Sessions.ForEach(s => s.Dispose());
|
Sessions.ForEach(s => s.Dispose());
|
||||||
|
|
||||||
Server?.Dispose();
|
Server?.Dispose();
|
||||||
Context?.Dispose();
|
|
||||||
HttpClient?.Dispose();
|
HttpClient?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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