219 lines
8.4 KiB
C#
219 lines
8.4 KiB
C#
using Fleck;
|
|
using SharpChat.Events;
|
|
using SharpChat.EventStorage;
|
|
using SharpChat.Packet;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace SharpChat {
|
|
public class ChatContext {
|
|
public HashSet<ChatChannel> Channels { get; } = new();
|
|
public readonly object ChannelsAccess = new();
|
|
|
|
public HashSet<ChatUserSession> Sessions { get; } = new();
|
|
public readonly object SessionsAccess = new();
|
|
|
|
public HashSet<ChatUser> Users { get; } = new();
|
|
public readonly object UsersAccess = new();
|
|
|
|
public IEventStorage Events { get; }
|
|
public readonly object EventsAccess = new();
|
|
|
|
public ChatContext(IEventStorage evtStore) {
|
|
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
|
|
}
|
|
|
|
public void Update() {
|
|
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 ChatUserSession GetSession(IWebSocketConnection conn) {
|
|
return Sessions.FirstOrDefault(s => s.Connection == conn);
|
|
}
|
|
|
|
public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
|
|
if(duration > TimeSpan.Zero)
|
|
user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration));
|
|
else
|
|
user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Kicked));
|
|
|
|
user.Close();
|
|
UserLeave(user.Channel, user, reason);
|
|
}
|
|
|
|
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.AddEvent(new UserConnectEvent(DateTimeOffset.Now, user, chan));
|
|
}
|
|
|
|
sess.Send(new AuthSuccessPacket(user, chan, sess, maxMsgLength));
|
|
sess.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
|
|
|
|
foreach(IChatEvent msg in Events.GetTargetEventLog(chan.Name))
|
|
sess.Send(new ContextMessagePacket(msg));
|
|
|
|
lock(ChannelsAccess)
|
|
sess.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank)));
|
|
|
|
if(!chan.HasUser(user))
|
|
chan.UserJoin(user);
|
|
|
|
lock(UsersAccess)
|
|
Users.Add(user);
|
|
}
|
|
}
|
|
|
|
public void UserLeave(ChatChannel chan, ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
|
|
user.Status = ChatUserStatus.Offline;
|
|
|
|
if(chan == null) {
|
|
foreach(ChatChannel channel in user.GetChannels()) {
|
|
UserLeave(channel, user, reason);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(chan.IsTemporary && chan.Owner == user)
|
|
lock(ChannelsAccess)
|
|
RemoveChannel(chan);
|
|
|
|
lock(EventsAccess) {
|
|
chan.UserLeave(user);
|
|
chan.Send(new UserDisconnectPacket(DateTimeOffset.Now, user, reason));
|
|
Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason));
|
|
}
|
|
}
|
|
|
|
public void SwitchChannel(ChatUser user, ChatChannel chan, string password) {
|
|
if(user.CurrentChannel == chan) {
|
|
//user.Send(true, "samechan", chan.Name);
|
|
user.ForceChannel();
|
|
return;
|
|
}
|
|
|
|
if(!user.Can(ChatUserPermissions.JoinAnyChannel) && chan.Owner != user) {
|
|
if(chan.Rank > user.Rank) {
|
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name));
|
|
user.ForceChannel();
|
|
return;
|
|
}
|
|
|
|
if(chan.Password != password) {
|
|
user.Send(new LegacyCommandResponse(LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name));
|
|
user.ForceChannel();
|
|
return;
|
|
}
|
|
}
|
|
|
|
ForceChannelSwitch(user, chan);
|
|
}
|
|
|
|
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.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan));
|
|
chan.Send(new UserChannelJoinPacket(user));
|
|
Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan));
|
|
|
|
user.Send(new ContextClearPacket(chan, ContextClearMode.MessagesUsers));
|
|
user.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
|
|
|
|
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)
|
|
lock(ChannelsAccess)
|
|
RemoveChannel(oldChan);
|
|
}
|
|
|
|
public void Send(IServerPacket packet) {
|
|
lock(UsersAccess)
|
|
foreach(ChatUser user in Users)
|
|
user.Send(packet);
|
|
}
|
|
|
|
public void UpdateChannel(ChatChannel channel, string name = null, bool? temporary = null, int? hierarchy = null, string password = null) {
|
|
if(channel == null)
|
|
throw new ArgumentNullException(nameof(channel));
|
|
if(!Channels.Contains(channel))
|
|
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
|
|
|
|
string prevName = channel.Name;
|
|
int prevHierarchy = channel.Rank;
|
|
bool nameUpdated = !string.IsNullOrWhiteSpace(name) && name != prevName;
|
|
|
|
if(nameUpdated) {
|
|
if(!ChatChannel.CheckName(name))
|
|
throw new ArgumentException("Name contains invalid characters.", nameof(name));
|
|
|
|
channel.Name = name;
|
|
}
|
|
|
|
if(temporary.HasValue)
|
|
channel.IsTemporary = temporary.Value;
|
|
|
|
if(hierarchy.HasValue)
|
|
channel.Rank = hierarchy.Value;
|
|
|
|
if(password != null)
|
|
channel.Password = password;
|
|
|
|
// Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively
|
|
lock(UsersAccess)
|
|
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) {
|
|
user.Send(new ChannelUpdatePacket(prevName, channel));
|
|
|
|
if(nameUpdated)
|
|
user.ForceChannel();
|
|
}
|
|
}
|
|
|
|
public void RemoveChannel(ChatChannel channel) {
|
|
if(channel == null || !Channels.Any())
|
|
return;
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|