sharp-chat/SharpChat.SockChat/SockChatContext.cs

523 lines
21 KiB
C#
Raw Normal View History

using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.SockChat;
using SharpChat.SockChat.PacketsS2C;
2022-08-30 15:00:58 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
2023-02-19 22:27:08 +00:00
using System.Threading;
2022-08-30 15:00:58 +00:00
namespace SharpChat {
2024-05-24 03:44:20 +00:00
public class SockChatContext : IChatEventHandler {
2023-02-19 22:27:08 +00:00
public readonly SemaphoreSlim ContextAccess = new(1, 1);
2022-08-30 15:00:58 +00:00
2024-05-19 21:02:17 +00:00
public ChannelsContext Channels { get; } = new();
2024-05-21 20:08:23 +00:00
public ConnectionsContext Connections { get; } = new();
2024-05-19 21:02:17 +00:00
public UsersContext Users { get; } = new();
2024-05-24 03:44:20 +00:00
public IEventStorage EventStorage { get; }
public ChatEventDispatcher Events { get; } = new();
2024-05-19 21:02:17 +00:00
public ChannelsUsersContext ChannelsUsers { get; } = new();
2023-02-19 22:27:08 +00:00
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
public SockChatContext(IEventStorage evtStore) {
2024-05-24 03:44:20 +00:00
EventStorage = evtStore;
Events.Subscribe(evtStore);
Events.Subscribe(this);
2022-08-30 15:00:58 +00:00
}
2024-05-24 03:44:20 +00:00
public void HandleEvent(ChatEventInfo info) {
// user status should be stored outside of the UserInfo class so we don't need to do this:
UserInfo? userInfo = Users.Get(info.SenderId);
switch(info.Type) {
case "user:connect":
SendTo(info.ChannelName, new UserConnectS2CPacket(
info.Id,
info.Created,
info.SenderId,
userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
));
break;
case "user:disconnect":
SendTo(info.ChannelName, new UserDisconnectS2CPacket(
info.Id,
info.Created,
info.SenderId,
userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave
));
break;
case "chan:join":
SendTo(info.ChannelName, new UserChannelJoinS2CPacket(
info.SenderId,
userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
));
break;
case "chan:leave":
SendTo(info.ChannelName, new UserChannelLeaveS2CPacket(info.SenderId));
break;
case "msg:delete":
if(info.Data is not MessageDeleteEventData msgDelete)
break;
2024-05-24 03:44:20 +00:00
MessageDeleteS2CPacket msgDelPacket = new(msgDelete.MessageId);
if(info.IsBroadcast) {
Send(msgDelPacket);
} else if(info.ChannelName.StartsWith('@')) {
long[] targetIds = info.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray();
if(targetIds.Length != 2)
return;
UserInfo[] users = Users.GetMany(targetIds);
UserInfo? target = users.FirstOrDefault(u => u.UserId != info.SenderId);
if(target == null)
return;
foreach(UserInfo user in users)
SendTo(user, msgDelPacket);
} else {
SendTo(info.ChannelName, msgDelPacket);
}
break;
case "msg:add":
if(info.Data is not MessageAddEventData msgAdd)
break;
if(info.IsBroadcast) {
Send(new MessageBroadcastS2CPacket(info.Id, info.Created, msgAdd.Text));
} else if(info.ChannelName.StartsWith('@')) {
// 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
long[] targetIds = info.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray();
if(targetIds.Length != 2)
return;
UserInfo[] users = Users.GetMany(targetIds);
UserInfo? target = users.FirstOrDefault(u => u.UserId != info.SenderId);
if(target == null)
return;
foreach(UserInfo user in users)
SendTo(user, new MessageAddS2CPacket(
info.Id,
DateTimeOffset.Now,
info.SenderId,
info.SenderId == user.UserId ? $"{SockChatUtility.GetUserName(target)} {msgAdd.Text}" : msgAdd.Text,
msgAdd.IsAction,
true
));
} else {
ChannelInfo? channel = Channels.Get(info.ChannelName, SockChatUtility.SanitiseChannelName);
if(channel != null)
SendTo(channel, new MessageAddS2CPacket(
info.Id,
DateTimeOffset.Now,
info.SenderId,
msgAdd.Text,
msgAdd.IsAction,
false
));
}
break;
}
}
2022-08-30 15:00:58 +00:00
public void Update() {
2024-05-21 20:08:23 +00:00
ConnectionInfo[] timedOut = Connections.GetTimedOut();
foreach(ConnectionInfo conn in timedOut) {
2024-05-20 16:16:32 +00:00
Connections.Remove(conn);
2024-05-21 20:08:23 +00:00
if(conn is SockChatConnectionInfo scConn)
scConn.Close(1002);
2023-02-19 22:27:08 +00:00
2024-05-20 16:16:32 +00:00
Logger.Write($"<{conn.RemoteEndPoint}> Nuked timed out connection from user #{conn.UserId}.");
}
2023-02-19 22:27:08 +00:00
2024-05-19 21:02:17 +00:00
foreach(UserInfo user in Users.All)
2024-05-20 16:16:32 +00:00
if(!Connections.HasUser(user)) {
HandleDisconnect(user, UserDisconnectReason.TimeOut);
2023-02-19 22:27:08 +00:00
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
}
public ChannelInfo[] GetUserChannels(UserInfo user) {
2024-05-19 21:02:17 +00:00
return Channels.GetMany(ChannelsUsers.GetUserChannelNames(user));
}
public UserInfo[] GetChannelUsers(ChannelInfo channel) {
2024-05-19 21:02:17 +00:00
return Users.GetMany(ChannelsUsers.GetChannelUserIds(channel));
}
public void UpdateUser(
UserInfo user,
2024-05-10 19:18:55 +00:00
string? userName = null,
string? nickName = null,
Colour? colour = null,
UserStatus? status = null,
2024-05-10 19:18:55 +00:00
string? statusText = null,
int? rank = null,
UserPermissions? perms = null,
2023-11-07 14:49:12 +00:00
bool? isSuper = null,
bool silent = false
) {
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.Equals(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;
}
if(isSuper.HasValue && user.IsSuper != isSuper) {
2023-11-07 14:49:12 +00:00
user.IsSuper = isSuper.Value;
hasChanged = true;
}
if(hasChanged) {
if(previousName != null)
SendToUserChannels(user, new UserUpdateNotificationS2CPacket(previousName, SockChatUtility.GetUserNameWithStatus(user)));
SendToUserChannels(user, new UserUpdateS2CPacket(
user.UserId,
2024-05-19 21:02:17 +00:00
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions
));
}
}
public void BanUser(UserInfo user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(duration > TimeSpan.Zero) {
DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
SendTo(user, new ForceDisconnectS2CPacket(expires));
} else
SendTo(user, new ForceDisconnectS2CPacket());
2024-05-21 20:08:23 +00:00
ConnectionInfo[] conns = Connections.GetUser(user);
foreach(ConnectionInfo conn in conns) {
2024-05-20 16:16:32 +00:00
Connections.Remove(conn);
2024-05-21 20:08:23 +00:00
if(conn is SockChatConnectionInfo scConn)
scConn.Close(1000);
2024-05-20 16:16:32 +00:00
Logger.Write($"<{conn.RemoteEndPoint}> Nuked connection from banned user #{conn.UserId}.");
}
2022-08-30 15:00:58 +00:00
HandleDisconnect(user, reason);
2022-08-30 15:00:58 +00:00
}
public void HandleChannelEventLog(string channelName, Action<ISockChatS2CPacket> handler) {
2024-05-24 03:44:20 +00:00
foreach(ChatEventInfo info in EventStorage.GetChannelEventLog(channelName)) {
switch(info.Type) {
2024-05-24 00:23:31 +00:00
case "msg:add":
if(info.Data is not MessageAddEventData msgAdd)
break;
2024-05-24 00:23:31 +00:00
handler(new MessageAddLogS2CPacket(
2024-05-24 03:44:20 +00:00
info.Id,
info.Created,
info.SenderId,
info.SenderName,
info.SenderColour,
info.SenderRank,
info.SenderPerms,
msgAdd.Text,
msgAdd.IsAction,
2024-05-24 03:44:20 +00:00
info.ChannelName.StartsWith('@'),
info.IsBroadcast,
2024-05-24 00:23:31 +00:00
false
));
2024-05-24 00:23:31 +00:00
break;
case "user:connect":
handler(new UserConnectLogS2CPacket(
2024-05-24 03:44:20 +00:00
info.Id,
info.Created,
SockChatUtility.GetUserName(info)
));
2024-05-24 00:23:31 +00:00
break;
case "user:disconnect":
handler(new UserDisconnectLogS2CPacket(
2024-05-24 03:44:20 +00:00
info.Id,
info.Created,
SockChatUtility.GetUserName(info),
info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave
));
2024-05-24 00:23:31 +00:00
break;
case "chan:join":
handler(new UserChannelJoinLogS2CPacket(
2024-05-24 03:44:20 +00:00
info.Id,
info.Created,
SockChatUtility.GetUserName(info)
));
2024-05-24 00:23:31 +00:00
break;
case "chan:leave":
handler(new UserChannelLeaveLogS2CPacket(
2024-05-24 03:44:20 +00:00
info.Id,
info.Created,
SockChatUtility.GetUserName(info)
));
2024-05-24 03:44:20 +00:00
break;
2024-05-24 00:23:31 +00:00
}
}
}
public void HandleJoin(UserInfo user, ChannelInfo chan, SockChatConnectionInfo conn, int maxMsgLength) {
2024-05-24 03:44:20 +00:00
if(!ChannelsUsers.Has(chan, user))
Events.Dispatch("user:connect", chan, user);
2022-08-30 15:00:58 +00:00
conn.Send(new AuthSuccessS2CPacket(
user.UserId,
2024-05-19 21:02:17 +00:00
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions,
chan.Name,
maxMsgLength
));
conn.Send(new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new UsersPopulateS2CPacket.ListEntry(
2024-05-19 21:02:17 +00:00
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions,
true
)
).OrderByDescending(user => user.Rank).ToArray()));
2022-08-30 15:00:58 +00:00
HandleChannelEventLog(chan.Name, p => conn.Send(p));
2022-08-30 15:00:58 +00:00
conn.Send(new ChannelsPopulateS2CPacket(Channels.GetMany(isPublic: true, minRank: user.Rank).Select(
channel => new ChannelsPopulateS2CPacket.ListEntry(channel.Name, channel.HasPassword, channel.IsTemporary)
).ToArray()));
2022-08-30 15:00:58 +00:00
2024-05-19 21:02:17 +00:00
if(Users.Get(userId: user.UserId) == null)
Users.Add(user);
2024-05-19 21:02:17 +00:00
ChannelsUsers.Join(chan.Name, user.UserId);
2022-08-30 15:00:58 +00:00
}
public void HandleDisconnect(UserInfo user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
UpdateUser(user, status: UserStatus.Offline);
Users.Remove(user.UserId);
2022-08-30 15:00:58 +00:00
ChannelInfo[] channels = GetUserChannels(user);
2024-05-19 21:02:17 +00:00
ChannelsUsers.DeleteUser(user);
2022-08-30 15:00:58 +00:00
foreach(ChannelInfo chan in channels) {
2024-05-24 03:44:20 +00:00
Events.Dispatch("user:disconnect", chan, user, new UserDisconnectEventData(reason));
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(UserInfo user, ChannelInfo chan, string password) {
2024-05-19 21:02:17 +00:00
if(ChannelsUsers.IsUserLastChannel(user, chan)) {
ForceChannel(user);
2022-08-30 15:00:58 +00:00
return;
}
if(!user.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
2023-02-07 15:01:56 +00:00
if(chan.Rank > user.Rank) {
SendTo(user, new ChannelRankTooLowErrorS2CPacket(chan.Name));
ForceChannel(user);
2022-08-30 15:00:58 +00:00
return;
}
2024-05-14 22:17:25 +00:00
if(!string.IsNullOrEmpty(chan.Password) && chan.Password.Equals(password)) {
SendTo(user, new ChannelPasswordWrongErrorS2CPacket(chan.Name));
ForceChannel(user);
2022-08-30 15:00:58 +00:00
return;
}
}
ForceChannelSwitch(user, chan);
}
public void ForceChannelSwitch(UserInfo user, ChannelInfo chan) {
2024-05-24 03:44:20 +00:00
DateTimeOffset now = DateTimeOffset.UtcNow;
2024-05-19 21:02:17 +00:00
ChannelInfo? oldChan = Channels.Get(ChannelsUsers.GetUserLastChannel(user));
2022-08-30 15:00:58 +00:00
2024-05-19 21:02:17 +00:00
if(oldChan != null)
2024-05-24 03:44:20 +00:00
Events.Dispatch("chan:leave", now, oldChan, user);
Events.Dispatch("chan:join", now, chan, user);
2022-08-30 15:00:58 +00:00
SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.ClearMode.MessagesUsers));
SendTo(user, new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new UsersPopulateS2CPacket.ListEntry(
2024-05-19 21:02:17 +00:00
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions,
true
)
).OrderByDescending(u => u.Rank).ToArray()));
2022-08-30 15:00:58 +00:00
HandleChannelEventLog(chan.Name, p => SendTo(user, p));
2023-02-19 22:27:08 +00:00
ForceChannel(user, chan);
2024-05-19 21:02:17 +00:00
if(oldChan != null)
ChannelsUsers.Leave(oldChan, user);
ChannelsUsers.Join(chan, user);
2022-08-30 15:00:58 +00:00
2024-05-19 21:02:17 +00:00
if(oldChan != null && 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(ISockChatS2CPacket packet) {
2024-05-20 16:24:14 +00:00
string data = packet.Pack();
2024-05-21 20:08:23 +00:00
Connections.WithAuthed(conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Send(data);
});
}
public void SendTo(UserInfo user, ISockChatS2CPacket packet) {
2024-05-20 16:24:14 +00:00
string data = packet.Pack();
2024-05-21 20:08:23 +00:00
Connections.WithUser(user, conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Send(data);
});
}
public void SendTo(ChannelInfo channel, ISockChatS2CPacket packet) {
2024-05-20 16:24:14 +00:00
SendTo(channel, packet.Pack());
}
public void SendTo(ChannelInfo channel, string packet) {
2024-05-24 03:44:20 +00:00
SendTo(channel.Name, packet);
}
public void SendTo(string channelName, ISockChatS2CPacket packet) {
SendTo(channelName, packet.Pack());
}
public void SendTo(string channelName, string packet) {
long[] userIds = ChannelsUsers.GetChannelUserIds(channelName);
2024-05-20 16:16:32 +00:00
foreach(long userId in userIds)
2024-05-21 20:08:23 +00:00
Connections.WithUser(userId, conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Send(packet);
});
}
public void SendToUserChannels(UserInfo user, ISockChatS2CPacket packet) {
2024-05-19 21:02:17 +00:00
ChannelInfo[] chans = GetUserChannels(user);
2024-05-20 16:24:14 +00:00
string data = packet.Pack();
2024-05-19 21:02:17 +00:00
foreach(ChannelInfo chan in chans)
2024-05-20 16:24:14 +00:00
SendTo(chan, data);
}
public void ForceChannel(UserInfo user, ChannelInfo? chan = null) {
2024-05-19 21:02:17 +00:00
chan ??= Channels.Get(ChannelsUsers.GetUserLastChannel(user));
if(chan != null)
SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name));
}
2022-08-30 15:00:58 +00:00
2024-05-10 19:18:55 +00:00
public void UpdateChannel(
ChannelInfo channel,
2024-05-10 19:18:55 +00:00
bool? temporary = null,
int? minRank = null,
string? password = null
) {
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
2024-05-19 21:02:17 +00:00
// the server currently doesn't keep track of what channels a user is already aware of so can't really simulate this yet.
foreach(UserInfo user in Users.GetMany(minRank: channel.Rank))
SendTo(user, new ChannelUpdateS2CPacket(prevName, channel.Name, channel.HasPassword, channel.IsTemporary));
2023-02-06 20:14:50 +00:00
}
2022-08-30 15:00:58 +00:00
public void RemoveChannel(ChannelInfo channel) {
2024-05-19 21:02:17 +00:00
if(channel == null || Channels.PublicCount > 1)
return;
2024-05-19 21:02:17 +00:00
ChannelInfo? defaultChannel = Channels.MainChannel;
if(defaultChannel == null)
2022-08-30 15:00:58 +00:00
return;
// Remove channel from the listing
2024-05-19 21:02:17 +00:00
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(UserInfo user in GetChannelUsers(channel))
SwitchChannel(user, defaultChannel, string.Empty);
// Broadcast deletion of channel
2024-05-19 21:02:17 +00:00
foreach(UserInfo user in Users.GetMany(minRank: channel.Rank))
SendTo(user, new ChannelDeleteS2CPacket(channel.Name));
2022-08-30 15:00:58 +00:00
}
}
}