sharp-chat/SharpChat/ChatContext.cs

440 lines
18 KiB
C#
Raw Normal View History

using SharpChat.Events;
using SharpChat.EventStorage;
2022-08-30 15:00:58 +00:00
using SharpChat.Packet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
2023-02-19 22:27:08 +00:00
using System.Threading;
2022-08-30 15:00:58 +00:00
namespace SharpChat {
public class ChatContext {
public record ChannelUserAssoc(long UserId, string ChannelName);
2023-02-19 22:27:08 +00:00
public readonly SemaphoreSlim ContextAccess = new(1, 1);
2022-08-30 15:00:58 +00:00
2023-02-19 22:27:08 +00:00
public HashSet<ChatChannel> Channels { get; } = new();
2023-02-16 21:25:41 +00:00
public HashSet<ChatConnection> Connections { get; } = new();
public HashSet<ChatUser> Users { get; } = new();
public IEventStorage Events { get; }
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = new();
2023-02-19 22:27:08 +00:00
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
public Dictionary<long, ChatChannel> UserLastChannel { get; } = new();
public ChatContext(IEventStorage evtStore) {
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
2022-08-30 15:00:58 +00:00
}
public void DispatchEvent(IChatEvent eventInfo) {
if(eventInfo is MessageCreateEvent mce) {
if(mce.IsBroadcast) {
Send(new BroadcastPacket(mce.MessageText));
} else if(mce.IsPrivate) {
// The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed
// e.g. nook sees @Arysil and Arysil sees @nook
// this entire routine is garbage, channels should probably in the db
2024-05-10 19:18:55 +00:00
if(mce.ChannelName?.StartsWith("@") != true)
return;
IEnumerable<long> uids = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1);
if(uids.Count() != 2)
return;
IEnumerable<ChatUser> users = Users.Where(u => uids.Any(uid => uid == u.UserId));
2024-05-10 19:18:55 +00:00
ChatUser? target = users.FirstOrDefault(u => u.UserId != mce.SenderId);
if(target == null)
return;
foreach(ChatUser user in users)
SendTo(user, new ChatMessageAddPacket(
mce.MessageId,
DateTimeOffset.Now,
mce.SenderId,
mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText,
mce.IsAction,
true
));
} else {
2024-05-10 19:18:55 +00:00
ChatChannel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
if(channel != null)
SendTo(channel, new ChatMessageAddPacket(
mce.MessageId,
DateTimeOffset.Now,
mce.SenderId,
mce.MessageText,
mce.IsAction,
false
));
}
Events.AddEvent(
mce.MessageId, "msg:add",
mce.ChannelName,
mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms,
new { text = mce.MessageText },
(mce.IsBroadcast ? StoredEventFlags.Broadcast : 0)
| (mce.IsAction ? StoredEventFlags.Action : 0)
| (mce.IsPrivate ? StoredEventFlags.Private : 0)
);
return;
}
}
2022-08-30 15:00:58 +00:00
public void Update() {
2023-02-19 22:27:08 +00:00
foreach(ChatConnection conn in Connections)
if(!conn.IsDisposed && conn.HasTimedOut) {
conn.Dispose();
Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
}
Connections.RemoveWhere(conn => conn.IsDisposed);
foreach(ChatUser user in Users)
if(!Connections.Any(conn => conn.User == user)) {
HandleDisconnect(user, UserDisconnectReason.TimeOut);
Logger.Write($"Timed out {user} (no more connections).");
}
}
public void SafeUpdate() {
ContextAccess.Wait();
try {
Update();
} finally {
ContextAccess.Release();
}
2022-08-30 15:00:58 +00:00
}
2024-05-10 19:18:55 +00:00
public bool IsInChannel(ChatUser? user, ChatChannel? channel) {
return user != null
&& channel != null
&& ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name));
}
public string[] GetUserChannelNames(ChatUser user) {
2023-02-19 22:27:08 +00:00
return ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName).ToArray();
}
public ChatChannel[] GetUserChannels(ChatUser user) {
string[] names = GetUserChannelNames(user);
2023-02-19 22:27:08 +00:00
return Channels.Where(c => names.Any(n => c.NameEquals(n))).ToArray();
}
public long[] GetChannelUserIds(ChatChannel channel) {
2023-02-19 22:27:08 +00:00
return ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId).ToArray();
}
public ChatUser[] GetChannelUsers(ChatChannel channel) {
long[] ids = GetChannelUserIds(channel);
2023-02-19 22:27:08 +00:00
return Users.Where(u => ids.Contains(u.UserId)).ToArray();
}
public void UpdateUser(
ChatUser user,
2024-05-10 19:18:55 +00:00
string? userName = null,
string? nickName = null,
ChatColour? colour = null,
ChatUserStatus? status = null,
2024-05-10 19:18:55 +00:00
string? statusText = null,
int? rank = null,
ChatUserPermissions? perms = null,
2023-11-07 14:49:12 +00:00
bool? isSuper = null,
bool silent = false
) {
if(user == null)
throw new ArgumentNullException(nameof(user));
bool hasChanged = false;
2024-05-10 19:18:55 +00:00
string? previousName = null;
if(userName != null && !user.UserName.Equals(userName)) {
user.UserName = userName;
hasChanged = true;
}
if(nickName != null && !user.NickName.Equals(nickName)) {
if(!silent)
previousName = string.IsNullOrWhiteSpace(user.NickName) ? user.UserName : user.NickName;
user.NickName = nickName;
hasChanged = true;
}
if(colour.HasValue && user.Colour != colour.Value) {
user.Colour = colour.Value;
hasChanged = true;
}
if(status.HasValue && user.Status != status.Value) {
user.Status = status.Value;
hasChanged = true;
}
if(statusText != null && !user.StatusText.Equals(statusText)) {
user.StatusText = statusText;
hasChanged = true;
}
if(rank != null && user.Rank != rank) {
user.Rank = (int)rank;
hasChanged = true;
}
if(perms.HasValue && user.Permissions != perms) {
user.Permissions = perms.Value;
hasChanged = true;
}
2023-11-07 14:49:12 +00:00
if(isSuper.HasValue) {
user.IsSuper = isSuper.Value;
hasChanged = true;
}
if(hasChanged) {
if(previousName != null)
SendToUserChannels(user, new UserUpdateNotificationPacket(previousName, user.LegacyNameWithStatus));
SendToUserChannels(user, new UserUpdatePacket(
user.UserId,
user.LegacyNameWithStatus,
user.Colour,
user.Rank,
user.Permissions
));
}
}
public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(duration > TimeSpan.Zero) {
DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
2024-05-10 17:28:52 +00:00
SendTo(user, new ForceDisconnectPacket(expires));
} else
2024-05-10 17:28:52 +00:00
SendTo(user, new ForceDisconnectPacket());
2023-02-19 22:27:08 +00:00
foreach(ChatConnection conn in Connections)
if(conn.User == user)
conn.Dispose();
Connections.RemoveWhere(conn => conn.IsDisposed);
2022-08-30 15:00:58 +00:00
HandleDisconnect(user, reason);
2022-08-30 15:00:58 +00:00
}
2023-02-16 21:25:41 +00:00
public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) {
2023-02-19 22:27:08 +00:00
if(!IsInChannel(user, chan)) {
SendTo(chan, new UserConnectPacket(
DateTimeOffset.Now,
user.UserId,
user.LegacyNameWithStatus,
user.Colour,
user.Rank,
user.Permissions
));
Events.AddEvent("user:connect", user, chan, flags: StoredEventFlags.Log);
2023-02-19 22:27:08 +00:00
}
2022-08-30 15:00:58 +00:00
conn.Send(new AuthSuccessPacket(
user.UserId,
user.LegacyNameWithStatus,
user.Colour,
user.Rank,
user.Permissions,
chan.Name,
maxMsgLength
));
conn.Send(new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new ContextUsersPacket.ListEntry(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions, true)
).OrderByDescending(user => user.Rank).ToArray()));
2022-08-30 15:00:58 +00:00
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
2023-02-19 22:27:08 +00:00
conn.Send(new ContextMessagePacket(msg));
2022-08-30 15:00:58 +00:00
conn.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank).Select(
channel => new ContextChannelsPacket.ListEntry(channel.Name, channel.HasPassword, channel.IsTemporary)
).ToArray()));
2022-08-30 15:00:58 +00:00
2023-02-19 22:27:08 +00:00
Users.Add(user);
2023-02-19 22:27:08 +00:00
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
UserLastChannel[user.UserId] = chan;
2022-08-30 15:00:58 +00:00
}
public void HandleDisconnect(ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
UpdateUser(user, status: ChatUserStatus.Offline);
2023-02-19 22:27:08 +00:00
Users.Remove(user);
UserLastChannel.Remove(user.UserId);
2022-08-30 15:00:58 +00:00
2023-02-19 22:27:08 +00:00
ChatChannel[] channels = GetUserChannels(user);
2022-08-30 15:00:58 +00:00
2023-02-19 22:27:08 +00:00
foreach(ChatChannel chan in channels) {
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));
SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, reason));
Events.AddEvent("user:disconnect", user, chan, new { reason = (int)reason }, StoredEventFlags.Log);
if(chan.IsTemporary && chan.IsOwner(user))
2023-02-19 22:27:08 +00:00
RemoveChannel(chan);
}
2022-08-30 15:00:58 +00:00
}
public void SwitchChannel(ChatUser user, ChatChannel chan, string password) {
2024-05-10 19:18:55 +00:00
if(UserLastChannel.TryGetValue(user.UserId, out ChatChannel? ulc) && chan == ulc) {
ForceChannel(user);
2022-08-30 15:00:58 +00:00
return;
}
if(!user.Can(ChatUserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
2023-02-07 15:01:56 +00:00
if(chan.Rank > user.Rank) {
SendTo(user, new LegacyCommandResponse(LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name));
ForceChannel(user);
2022-08-30 15:00:58 +00:00
return;
}
if(!string.IsNullOrEmpty(chan.Password) && chan.Password != password) {
SendTo(user, new LegacyCommandResponse(LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name));
ForceChannel(user);
2022-08-30 15:00:58 +00:00
return;
}
}
ForceChannelSwitch(user, chan);
}
public void ForceChannelSwitch(ChatUser user, ChatChannel chan) {
2023-02-19 22:27:08 +00:00
if(!Channels.Contains(chan))
return;
2022-08-30 15:00:58 +00:00
ChatChannel oldChan = UserLastChannel[user.UserId];
2022-08-30 15:00:58 +00:00
SendTo(oldChan, new UserChannelLeavePacket(user.UserId));
Events.AddEvent("chan:leave", user, oldChan, flags: StoredEventFlags.Log);
SendTo(chan, new UserChannelJoinPacket(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
Events.AddEvent("chan:join", user, oldChan, flags: StoredEventFlags.Log);
2022-08-30 15:00:58 +00:00
SendTo(user, new ContextClearPacket(ContextClearPacket.ClearMode.MessagesUsers));
SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new ContextUsersPacket.ListEntry(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions, true)
).OrderByDescending(u => u.Rank).ToArray()));
2022-08-30 15:00:58 +00:00
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
2023-02-19 22:27:08 +00:00
SendTo(user, new ContextMessagePacket(msg));
2022-08-30 15:00:58 +00:00
2023-02-19 22:27:08 +00:00
ForceChannel(user, chan);
2023-02-19 22:27:08 +00:00
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name));
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
UserLastChannel[user.UserId] = chan;
2022-08-30 15:00:58 +00:00
if(oldChan.IsTemporary && oldChan.IsOwner(user))
2023-02-19 22:27:08 +00:00
RemoveChannel(oldChan);
2022-08-30 15:00:58 +00:00
}
public void Send(IServerPacket packet) {
if(packet == null)
throw new ArgumentNullException(nameof(packet));
2023-02-19 22:27:08 +00:00
foreach(ChatConnection conn in Connections)
if(conn.IsAuthed)
conn.Send(packet);
}
public void SendTo(ChatUser user, IServerPacket packet) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
2023-02-19 22:27:08 +00:00
foreach(ChatConnection conn in Connections)
if(conn.IsAlive && conn.User == user)
conn.Send(packet);
}
public void SendTo(ChatChannel channel, IServerPacket packet) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
// might be faster to grab the users first and then cascade into that SendTo
2023-02-19 22:27:08 +00:00
IEnumerable<ChatConnection> conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel));
foreach(ChatConnection conn in conns)
conn.Send(packet);
}
public void SendToUserChannels(ChatUser user, IServerPacket packet) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(packet == null)
throw new ArgumentNullException(nameof(packet));
IEnumerable<ChatChannel> chans = Channels.Where(c => IsInChannel(user, c));
2024-05-10 19:18:55 +00:00
IEnumerable<ChatConnection> conns = Connections.Where(conn => conn.IsAuthed && ChannelUsers.Any(cu => cu.UserId == conn.User?.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName))));
foreach(ChatConnection conn in conns)
conn.Send(packet);
}
public IPAddress[] GetRemoteAddresses(ChatUser user) {
2023-02-19 22:27:08 +00:00
return Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct().ToArray();
}
2024-05-10 19:18:55 +00:00
public void ForceChannel(ChatUser user, ChatChannel? chan = null) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
throw new ArgumentException("no channel???");
SendTo(user, new UserChannelForceJoinPacket(chan.Name));
}
2022-08-30 15:00:58 +00:00
2024-05-10 19:18:55 +00:00
public void UpdateChannel(
ChatChannel channel,
bool? temporary = null,
int? minRank = 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));
2022-08-30 15:00:58 +00:00
string prevName = channel.Name;
if(temporary.HasValue)
channel.IsTemporary = temporary.Value;
if(minRank.HasValue)
channel.Rank = minRank.Value;
if(password != null)
channel.Password = password;
2022-08-30 15:00:58 +00:00
// TODO: Users that no longer have access to the channel/gained access to the channel by the rank change should receive delete and create packets respectively
2023-02-19 22:27:08 +00:00
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) {
SendTo(user, new ChannelUpdatePacket(prevName, channel.Name, channel.HasPassword, channel.IsTemporary));
2023-02-19 22:27:08 +00:00
}
2023-02-06 20:14:50 +00:00
}
2022-08-30 15:00:58 +00:00
public void RemoveChannel(ChatChannel channel) {
if(channel == null || !Channels.Any())
return;
2024-05-10 19:18:55 +00:00
ChatChannel? defaultChannel = Channels.FirstOrDefault();
if(defaultChannel == null)
2022-08-30 15:00:58 +00:00
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 GetChannelUsers(channel))
SwitchChannel(user, defaultChannel, string.Empty);
// Broadcast deletion of channel
2023-02-19 22:27:08 +00:00
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank))
SendTo(user, new ChannelDeletePacket(channel.Name));
2022-08-30 15:00:58 +00:00
}
}
}