451 lines
18 KiB
C#
451 lines
18 KiB
C#
using SharpChat.Events;
|
|
using SharpChat.EventStorage;
|
|
using SharpChat.Packet;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Threading;
|
|
|
|
namespace SharpChat {
|
|
public class ChatContext {
|
|
public readonly SemaphoreSlim ContextAccess = new(1, 1);
|
|
|
|
public ChannelsContext Channels { get; } = new();
|
|
public List<ConnectionInfo> Connections { get; } = new();
|
|
public UsersContext Users { get; } = new();
|
|
public IEventStorage Events { get; }
|
|
public ChannelsUsersContext ChannelsUsers { get; } = new();
|
|
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
|
|
|
|
public ChatContext(IEventStorage evtStore) {
|
|
Events = evtStore;
|
|
}
|
|
|
|
public void DispatchEvent(IChatEvent eventInfo) {
|
|
if(eventInfo is MessageCreateEvent mce) {
|
|
if(mce.IsBroadcast) {
|
|
Send(new MessageBroadcastPacket(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
|
|
if(mce.ChannelName?.StartsWith("@") != true)
|
|
return;
|
|
|
|
long[] targetIds = mce.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 != mce.SenderId);
|
|
if(target == null)
|
|
return;
|
|
|
|
foreach(UserInfo user in users)
|
|
SendTo(user, new MessageAddPacket(
|
|
mce.MessageId,
|
|
DateTimeOffset.Now,
|
|
mce.SenderId,
|
|
mce.SenderId == user.UserId ? $"{SockChatUtility.GetUserName(target)} {mce.MessageText}" : mce.MessageText,
|
|
mce.IsAction,
|
|
true
|
|
));
|
|
} else {
|
|
ChannelInfo? channel = Channels.Get(mce.ChannelName, SockChatUtility.SanitiseChannelName);
|
|
if(channel != null)
|
|
SendTo(channel, new MessageAddPacket(
|
|
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;
|
|
}
|
|
}
|
|
|
|
public void Update() {
|
|
foreach(ConnectionInfo conn in Connections)
|
|
if(!conn.IsDisposed && conn.HasTimedOut) {
|
|
conn.Dispose();
|
|
Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
|
|
}
|
|
|
|
int removed = Connections.RemoveAll(conn => conn.IsDisposed);
|
|
if(removed > 0)
|
|
Logger.Write($"Removed {removed} nuked connections from the list.");
|
|
|
|
foreach(UserInfo user in Users.All)
|
|
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();
|
|
}
|
|
}
|
|
|
|
public ChannelInfo[] GetUserChannels(UserInfo user) {
|
|
return Channels.GetMany(ChannelsUsers.GetUserChannelNames(user));
|
|
}
|
|
|
|
public UserInfo[] GetChannelUsers(ChannelInfo channel) {
|
|
return Users.GetMany(ChannelsUsers.GetChannelUserIds(channel));
|
|
}
|
|
|
|
public void UpdateUser(
|
|
UserInfo user,
|
|
string? userName = null,
|
|
string? nickName = null,
|
|
Colour? colour = null,
|
|
UserStatus? status = null,
|
|
string? statusText = null,
|
|
int? rank = null,
|
|
UserPermissions? perms = null,
|
|
bool? isSuper = null,
|
|
bool silent = false
|
|
) {
|
|
bool hasChanged = false;
|
|
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.Value;
|
|
hasChanged = true;
|
|
}
|
|
|
|
if(hasChanged) {
|
|
if(previousName != null)
|
|
SendToUserChannels(user, new UserUpdateNotificationPacket(previousName, SockChatUtility.GetUserNameWithStatus(user)));
|
|
|
|
SendToUserChannels(user, new UserUpdatePacket(
|
|
user.UserId,
|
|
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 ForceDisconnectPacket(expires));
|
|
} else
|
|
SendTo(user, new ForceDisconnectPacket());
|
|
|
|
foreach(ConnectionInfo conn in Connections)
|
|
if(conn.User == user)
|
|
conn.Dispose();
|
|
Connections.RemoveAll(conn => conn.IsDisposed);
|
|
|
|
HandleDisconnect(user, reason);
|
|
}
|
|
|
|
public void HandleChannelEventLog(string channelName, Action<ServerPacket> handler) {
|
|
foreach(StoredEventInfo msg in Events.GetChannelEventLog(channelName))
|
|
handler(msg.Type switch {
|
|
"msg:add" => new MessageAddLogPacket(
|
|
msg.Id,
|
|
msg.Created,
|
|
msg.Sender?.UserId ?? -1,
|
|
msg.Sender == null ? "ChatBot" : SockChatUtility.GetUserName(msg.Sender),
|
|
msg.Sender?.Colour ?? Colour.None,
|
|
msg.Sender?.Rank ?? 0,
|
|
msg.Sender?.Permissions ?? 0,
|
|
msg.Data.RootElement.GetProperty("text").GetString() ?? string.Empty,
|
|
msg.Flags.HasFlag(StoredEventFlags.Action),
|
|
msg.Flags.HasFlag(StoredEventFlags.Private),
|
|
msg.Flags.HasFlag(StoredEventFlags.Broadcast),
|
|
false
|
|
),
|
|
"user:connect" => new UserConnectLogPacket(
|
|
msg.Created,
|
|
msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender)
|
|
),
|
|
"user:disconnect" => new UserDisconnectLogPacket(
|
|
msg.Created,
|
|
msg.Sender == null ? string.Empty : SockChatUtility.GetUserNameWithStatus(msg.Sender),
|
|
(UserDisconnectReason)msg.Data.RootElement.GetProperty("reason").GetByte()
|
|
),
|
|
"chan:join" => new UserChannelJoinLogPacket(
|
|
msg.Created,
|
|
msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender)
|
|
),
|
|
"chan:leave" => new UserChannelLeaveLogPacket(
|
|
msg.Created,
|
|
msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender)
|
|
),
|
|
_ => throw new Exception($"Unsupported backlog type: {msg.Type}"),
|
|
});
|
|
}
|
|
|
|
public void HandleJoin(UserInfo user, ChannelInfo chan, ConnectionInfo conn, int maxMsgLength) {
|
|
if(!ChannelsUsers.Has(chan, user)) {
|
|
SendTo(chan, new UserConnectPacket(
|
|
DateTimeOffset.Now,
|
|
user.UserId,
|
|
SockChatUtility.GetUserNameWithStatus(user),
|
|
user.Colour,
|
|
user.Rank,
|
|
user.Permissions
|
|
));
|
|
Events.AddEvent("user:connect", user, chan, flags: StoredEventFlags.Log);
|
|
}
|
|
|
|
conn.Send(new AuthSuccessPacket(
|
|
user.UserId,
|
|
SockChatUtility.GetUserNameWithStatus(user),
|
|
user.Colour,
|
|
user.Rank,
|
|
user.Permissions,
|
|
chan.Name,
|
|
maxMsgLength
|
|
));
|
|
conn.Send(new UsersPopulatePacket(GetChannelUsers(chan).Except(new[] { user }).Select(
|
|
user => new UsersPopulatePacket.ListEntry(
|
|
user.UserId,
|
|
SockChatUtility.GetUserNameWithStatus(user),
|
|
user.Colour,
|
|
user.Rank,
|
|
user.Permissions,
|
|
true
|
|
)
|
|
).OrderByDescending(user => user.Rank).ToArray()));
|
|
|
|
HandleChannelEventLog(chan.Name, p => conn.Send(p));
|
|
|
|
conn.Send(new ChannelsPopulatePacket(Channels.GetMany(isPublic: true, minRank: user.Rank).Select(
|
|
channel => new ChannelsPopulatePacket.ListEntry(channel.Name, channel.HasPassword, channel.IsTemporary)
|
|
).ToArray()));
|
|
|
|
if(Users.Get(userId: user.UserId) == null)
|
|
Users.Add(user);
|
|
|
|
ChannelsUsers.Join(chan.Name, user.UserId);
|
|
}
|
|
|
|
public void HandleDisconnect(UserInfo user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
|
|
UpdateUser(user, status: UserStatus.Offline);
|
|
Users.Remove(user.UserId);
|
|
|
|
ChannelInfo[] channels = GetUserChannels(user);
|
|
ChannelsUsers.DeleteUser(user);
|
|
|
|
foreach(ChannelInfo chan in channels) {
|
|
SendTo(chan, new UserDisconnectPacket(
|
|
DateTimeOffset.Now,
|
|
user.UserId,
|
|
SockChatUtility.GetUserNameWithStatus(user),
|
|
reason
|
|
));
|
|
Events.AddEvent("user:disconnect", user, chan, new { reason = (int)reason }, StoredEventFlags.Log);
|
|
|
|
if(chan.IsTemporary && chan.IsOwner(user))
|
|
RemoveChannel(chan);
|
|
}
|
|
}
|
|
|
|
public void SwitchChannel(UserInfo user, ChannelInfo chan, string password) {
|
|
if(ChannelsUsers.IsUserLastChannel(user, chan)) {
|
|
ForceChannel(user);
|
|
return;
|
|
}
|
|
|
|
if(!user.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
|
|
if(chan.Rank > user.Rank) {
|
|
SendTo(user, new ChannelRankTooLowErrorPacket(chan.Name));
|
|
ForceChannel(user);
|
|
return;
|
|
}
|
|
|
|
if(!string.IsNullOrEmpty(chan.Password) && chan.Password.Equals(password)) {
|
|
SendTo(user, new ChannelPasswordWrongErrorPacket(chan.Name));
|
|
ForceChannel(user);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ForceChannelSwitch(user, chan);
|
|
}
|
|
|
|
public void ForceChannelSwitch(UserInfo user, ChannelInfo chan) {
|
|
ChannelInfo? oldChan = Channels.Get(ChannelsUsers.GetUserLastChannel(user));
|
|
|
|
if(oldChan != null) {
|
|
SendTo(oldChan, new UserChannelLeavePacket(user.UserId));
|
|
Events.AddEvent("chan:leave", user, oldChan, flags: StoredEventFlags.Log);
|
|
}
|
|
|
|
SendTo(chan, new UserChannelJoinPacket(
|
|
user.UserId,
|
|
SockChatUtility.GetUserNameWithStatus(user),
|
|
user.Colour,
|
|
user.Rank,
|
|
user.Permissions
|
|
));
|
|
|
|
if(oldChan != null)
|
|
Events.AddEvent("chan:join", user, oldChan, flags: StoredEventFlags.Log);
|
|
|
|
SendTo(user, new ContextClearPacket(ContextClearPacket.ClearMode.MessagesUsers));
|
|
SendTo(user, new UsersPopulatePacket(GetChannelUsers(chan).Except(new[] { user }).Select(
|
|
user => new UsersPopulatePacket.ListEntry(
|
|
user.UserId,
|
|
SockChatUtility.GetUserNameWithStatus(user),
|
|
user.Colour,
|
|
user.Rank,
|
|
user.Permissions,
|
|
true
|
|
)
|
|
).OrderByDescending(u => u.Rank).ToArray()));
|
|
|
|
HandleChannelEventLog(chan.Name, p => SendTo(user, p));
|
|
ForceChannel(user, chan);
|
|
|
|
if(oldChan != null)
|
|
ChannelsUsers.Leave(oldChan, user);
|
|
ChannelsUsers.Join(chan, user);
|
|
|
|
if(oldChan != null && oldChan.IsTemporary && oldChan.IsOwner(user))
|
|
RemoveChannel(oldChan);
|
|
}
|
|
|
|
public void Send(ServerPacket packet) {
|
|
foreach(ConnectionInfo conn in Connections)
|
|
if(conn.IsAuthed)
|
|
conn.Send(packet);
|
|
}
|
|
|
|
public void SendTo(UserInfo user, ServerPacket packet) {
|
|
foreach(ConnectionInfo conn in Connections)
|
|
if(conn.IsAuthed && conn.User!.UserId == user.UserId)
|
|
conn.Send(packet);
|
|
}
|
|
|
|
public void SendTo(ChannelInfo channel, ServerPacket packet) {
|
|
long[] userIds = ChannelsUsers.GetChannelUserIds(channel);
|
|
foreach(ConnectionInfo conn in Connections)
|
|
if(conn.IsAuthed && userIds.Contains(conn.User!.UserId))
|
|
conn.Send(packet);
|
|
}
|
|
|
|
public void SendToUserChannels(UserInfo user, ServerPacket packet) {
|
|
ChannelInfo[] chans = GetUserChannels(user);
|
|
foreach(ChannelInfo chan in chans)
|
|
SendTo(chan, packet);
|
|
}
|
|
|
|
public IPAddress[] GetRemoteAddresses(UserInfo user) {
|
|
return Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct().ToArray();
|
|
}
|
|
|
|
public void ForceChannel(UserInfo user, ChannelInfo? chan = null) {
|
|
chan ??= Channels.Get(ChannelsUsers.GetUserLastChannel(user));
|
|
if(chan != null)
|
|
SendTo(user, new UserChannelForceJoinPacket(chan.Name));
|
|
}
|
|
|
|
public void UpdateChannel(
|
|
ChannelInfo channel,
|
|
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;
|
|
|
|
// 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
|
|
// 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 ChannelUpdatePacket(prevName, channel.Name, channel.HasPassword, channel.IsTemporary));
|
|
}
|
|
|
|
public void RemoveChannel(ChannelInfo channel) {
|
|
if(channel == null || Channels.PublicCount > 1)
|
|
return;
|
|
|
|
ChannelInfo? defaultChannel = Channels.MainChannel;
|
|
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(UserInfo user in GetChannelUsers(channel))
|
|
SwitchChannel(user, defaultChannel, string.Empty);
|
|
|
|
// Broadcast deletion of channel
|
|
foreach(UserInfo user in Users.GetMany(minRank: channel.Rank))
|
|
SendTo(user, new ChannelDeletePacket(channel.Name));
|
|
}
|
|
}
|
|
}
|