2025-04-28 12:29:11 +00:00
using Microsoft.Extensions.Logging ;
2025-04-28 13:03:38 +00:00
using SharpChat.Auth ;
using SharpChat.Bans ;
2025-04-27 02:53:56 +00:00
using SharpChat.Channels ;
2025-04-28 13:03:38 +00:00
using SharpChat.Configuration ;
2025-05-03 02:49:51 +00:00
using SharpChat.Connections ;
2025-04-26 19:42:23 +00:00
using SharpChat.Events ;
2025-04-27 01:54:46 +00:00
using SharpChat.Messages ;
2025-05-03 02:49:51 +00:00
using SharpChat.Sessions ;
2025-04-25 20:05:55 +00:00
using SharpChat.Snowflake ;
2025-04-26 22:47:57 +00:00
using SharpChat.SockChat ;
using SharpChat.SockChat.S2CPackets ;
2025-05-03 02:49:51 +00:00
using SharpChat.Storage ;
using SharpChat.Users ;
2025-04-27 18:45:32 +00:00
using System.Dynamic ;
2023-02-16 23:33:48 +01:00
using System.Net ;
2025-04-28 12:29:11 +00:00
using ZLogger ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
namespace SharpChat ;
2025-04-25 20:05:55 +00:00
2025-04-26 23:15:54 +00:00
public class Context {
2025-04-28 13:03:38 +00:00
public const int DEFAULT_MSG_LENGTH_MAX = 5000 ;
public const int DEFAULT_MAX_CONNECTIONS = 5 ;
public const int DEFAULT_FLOOD_KICK_LENGTH = 30 ;
public const int DEFAULT_FLOOD_KICK_EXEMPT_RANK = 9 ;
2025-04-26 23:15:54 +00:00
public readonly SemaphoreSlim ContextAccess = new ( 1 , 1 ) ;
2023-02-19 23:27:08 +01:00
2025-04-28 12:29:11 +00:00
public ILoggerFactory LoggerFactory { get ; }
2025-04-28 13:03:38 +00:00
public Config Config { get ; }
public MessageStorage Messages { get ; }
public AuthClient Auth { get ; }
public BansClient Bans { get ; }
public CachedValue < int > MaxMessageLength { get ; }
public CachedValue < int > MaxConnections { get ; }
public CachedValue < int > FloodKickLength { get ; }
public CachedValue < int > FloodKickExemptRank { get ; }
2025-04-28 12:29:11 +00:00
private readonly ILogger Logger ;
2025-04-26 23:15:54 +00:00
public SnowflakeGenerator SnowflakeGenerator { get ; } = new ( ) ;
public RandomSnowflake RandomSnowflake { get ; }
2023-02-19 23:27:08 +01:00
2025-05-03 02:49:51 +00:00
public UsersContext Users { get ; } = new ( ) ;
public SessionsContext Sessions { get ; }
public ChannelsContext Channels { get ; }
public ChannelsUsersContext ChannelsUsers { get ; }
2025-04-26 23:15:54 +00:00
public Dictionary < string , RateLimiter > UserRateLimiters { get ; } = [ ] ;
2023-02-19 23:27:08 +01:00
2025-04-28 13:03:38 +00:00
public Context (
2025-05-03 02:49:51 +00:00
ILoggerFactory loggerFactory ,
2025-04-28 13:03:38 +00:00
Config config ,
2025-05-03 02:49:51 +00:00
StorageBackend storage ,
2025-04-28 13:03:38 +00:00
AuthClient authClient ,
BansClient bansClient
) {
2025-05-03 02:49:51 +00:00
LoggerFactory = loggerFactory ;
Logger = loggerFactory . CreateLogger ( "ctx" ) ;
2025-04-28 13:03:38 +00:00
Config = config ;
Messages = storage . CreateMessageStorage ( ) ;
Auth = authClient ;
Bans = bansClient ;
2025-05-03 02:49:51 +00:00
2025-04-26 23:15:54 +00:00
RandomSnowflake = new ( SnowflakeGenerator ) ;
2025-05-03 02:49:51 +00:00
Sessions = new ( loggerFactory , RandomSnowflake ) ;
Channels = new ( RandomSnowflake ) ;
ChannelsUsers = new ( Channels , Users ) ;
2025-04-28 13:03:38 +00:00
Logger . ZLogDebug ( $"Reading cached config values..." ) ;
MaxMessageLength = config . ReadCached ( "msgMaxLength" , DEFAULT_MSG_LENGTH_MAX ) ;
MaxConnections = config . ReadCached ( "connMaxCount" , DEFAULT_MAX_CONNECTIONS ) ;
FloodKickLength = config . ReadCached ( "floodKickLength" , DEFAULT_FLOOD_KICK_LENGTH ) ;
FloodKickExemptRank = config . ReadCached ( "floodKickExemptRank" , DEFAULT_FLOOD_KICK_EXEMPT_RANK ) ;
Logger . ZLogDebug ( $"Creating channel list..." ) ;
string [ ] channelNames = config . ReadValue < string [ ] > ( "channels" ) ? ? [ "lounge" ] ;
if ( channelNames is not null )
foreach ( string channelName in channelNames ) {
Config channelCfg = config . ScopeTo ( $"channels:{channelName}" ) ;
string name = channelCfg . SafeReadValue ( "name" , string . Empty ) ! ;
if ( string . IsNullOrWhiteSpace ( name ) )
name = channelName ;
Channels . CreateChannel (
name ,
channelCfg . SafeReadValue ( "password" , string . Empty ) ! ,
rank : channelCfg . SafeReadValue ( "minRank" , 0 )
) ;
}
2025-04-26 23:15:54 +00:00
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
public async Task DispatchEvent ( ChatEvent eventInfo ) {
if ( eventInfo is MessageCreateEvent mce ) {
if ( mce . IsBroadcast ) {
await Send ( new CommandResponseS2CPacket ( RandomSnowflake . Next ( ) , LCR . BROADCAST , false , 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
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
// this entire routine is garbage, channels should probably in the db
if ( ! mce . ChannelName . StartsWith ( '@' ) )
return ;
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
IEnumerable < string > uids = mce . ChannelName [ 1. . ] . Split ( '-' , 3 ) . Select ( u = > ( long . TryParse ( u , out long up ) ? up : - 1 ) . ToString ( ) ) ;
if ( uids . Count ( ) ! = 2 )
return ;
2023-02-17 20:02:35 +01:00
2025-05-03 02:49:51 +00:00
IEnumerable < User > users = Users . GetUsers ( uids ) ;
User ? target = users . FirstOrDefault ( u = > mce . SenderId . Equals ( u . UserId , StringComparison . Ordinal ) ) ;
2025-04-26 23:15:54 +00:00
if ( target = = null )
return ;
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
foreach ( User user in users )
await SendTo ( user , new ChatMessageAddS2CPacket (
mce . MessageId ,
DateTimeOffset . Now ,
mce . SenderId ,
2025-05-03 02:49:51 +00:00
mce . SenderId = = user . UserId ? $"{target.GetLegacyName()} {mce.MessageText}" : mce . MessageText ,
2025-04-26 23:15:54 +00:00
mce . IsAction ,
true
) ) ;
} else {
2025-04-27 02:53:56 +00:00
Channel ? channel = Channels . GetChannel ( mce . ChannelName ) ;
2025-04-26 23:15:54 +00:00
if ( channel is not null )
await SendTo ( channel , new ChatMessageAddS2CPacket (
mce . MessageId ,
DateTimeOffset . Now ,
mce . SenderId ,
mce . MessageText ,
mce . IsAction ,
false
) ) ;
}
2025-04-27 18:45:32 +00:00
dynamic data = new ExpandoObject ( ) ;
data . text = mce . MessageText ;
if ( mce . IsAction )
data . act = true ;
await Messages . LogMessage ( mce . MessageId , "msg:add" , mce . ChannelName , mce . SenderId , mce . SenderName , mce . SenderColour , mce . SenderRank , mce . SenderNickName , mce . SenderPerms , data ) ;
2025-04-26 23:15:54 +00:00
return ;
2023-02-17 20:02:35 +01:00
}
2025-04-26 23:15:54 +00:00
}
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
public async Task Update ( ) {
2025-05-03 02:49:51 +00:00
foreach ( Session session in Sessions . GetTimedOutSessions ( ) ) {
session . Logger . ZLogInformation ( $"Nuking connection associated with user #{session.UserId}" ) ;
session . Connection . Close ( ConnectionCloseReason . TimeOut ) ;
Sessions . DestroySession ( session ) ;
}
2023-02-22 01:28:53 +01:00
2025-05-03 02:49:51 +00:00
foreach ( User user in Users . GetUsers ( ) )
if ( Sessions . CountActiveSessions ( user ) < 1 ) {
2025-04-28 12:29:11 +00:00
Logger . ZLogInformation ( $"Timing out user {user.UserId} (no more connections)." ) ;
2025-04-26 23:15:54 +00:00
await HandleDisconnect ( user , UserDisconnectS2CPacket . Reason . TimeOut ) ;
2023-02-22 01:28:53 +01:00
}
2025-04-26 23:15:54 +00:00
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public async Task SafeUpdate ( ) {
ContextAccess . Wait ( ) ;
try {
await Update ( ) ;
} finally {
ContextAccess . Release ( ) ;
}
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public async Task UpdateUser (
User user ,
string? userName = null ,
string? nickName = null ,
ColourInheritable ? colour = null ,
UserStatus ? status = null ,
string? statusText = null ,
int? rank = null ,
UserPermissions ? perms = null ,
bool silent = false
) {
2025-05-03 02:49:51 +00:00
string previousName = user . GetLegacyName ( ) ;
UserDiff diff = Users . UpdateUser (
user ,
userName ,
colour ,
rank ,
perms ,
nickName ,
status ,
statusText
) ;
2022-08-30 17:00:58 +02:00
2025-05-03 02:49:51 +00:00
if ( diff . Changed ) {
string currentName = user . GetLegacyNameWithStatus ( ) ;
2022-08-30 17:00:58 +02:00
2025-05-03 02:49:51 +00:00
if ( ! silent & & diff . Nick . Changed )
await SendToUserChannels ( user , new CommandResponseS2CPacket ( RandomSnowflake . Next ( ) , LCR . NICKNAME_CHANGE , false , previousName , currentName ) ) ;
2022-08-30 17:00:58 +02:00
2025-05-03 02:49:51 +00:00
await SendToUserChannels ( user , new UserUpdateS2CPacket ( diff . Id , currentName , diff . Colour . After , diff . Rank . After , diff . Permissions . After ) ) ;
2022-08-30 17:00:58 +02:00
}
2025-04-26 23:15:54 +00:00
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
public async Task BanUser ( User user , TimeSpan duration , UserDisconnectS2CPacket . Reason reason = UserDisconnectS2CPacket . Reason . Kicked ) {
if ( duration > TimeSpan . Zero ) {
DateTimeOffset expires = duration > = TimeSpan . MaxValue ? DateTimeOffset . MaxValue : DateTimeOffset . Now + duration ;
await SendTo ( user , new ForceDisconnectS2CPacket ( expires ) ) ;
} else
await SendTo ( user , new ForceDisconnectS2CPacket ( ) ) ;
2022-08-30 17:00:58 +02:00
2025-05-03 02:49:51 +00:00
foreach ( SockChatConnection conn in Sessions . GetConnections < SockChatConnection > ( user ) ) {
conn . Close ( ConnectionCloseReason . Unauthorized ) ;
Sessions . DestroySession ( conn ) ;
}
2022-08-30 17:00:58 +02:00
2025-05-03 02:49:51 +00:00
await Update ( ) ;
2025-04-26 23:15:54 +00:00
await HandleDisconnect ( user , reason ) ;
}
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
public async Task HandleDisconnect ( User user , UserDisconnectS2CPacket . Reason reason = UserDisconnectS2CPacket . Reason . Leave ) {
await UpdateUser ( user , status : UserStatus . Offline ) ;
2025-05-03 02:49:51 +00:00
Users . RemoveUser ( user ) ;
2025-04-26 23:15:54 +00:00
2025-05-03 02:49:51 +00:00
foreach ( Channel chan in ChannelsUsers . GetUserChannels ( user ) ) {
ChannelsUsers . RemoveChannelUser ( chan , user ) ;
2025-04-26 23:15:54 +00:00
long msgId = RandomSnowflake . Next ( ) ;
2025-05-03 02:49:51 +00:00
await SendTo ( chan , new UserDisconnectS2CPacket ( msgId , DateTimeOffset . Now , user . UserId , user . GetLegacyNameWithStatus ( ) , reason ) ) ;
2025-04-27 18:45:32 +00:00
await Messages . LogMessage ( msgId , "user:disconnect" , chan . Name , user . UserId , user . UserName , user . Colour , user . Rank , user . NickName , user . Permissions , new { reason = ( int ) reason } ) ;
2025-04-26 23:15:54 +00:00
2025-04-27 01:54:46 +00:00
if ( chan . IsTemporary & & chan . IsOwner ( user . UserId ) )
2025-04-26 23:15:54 +00:00
await RemoveChannel ( chan ) ;
2022-08-30 17:00:58 +02:00
}
2025-05-03 02:49:51 +00:00
ChannelsUsers . RemoveUser ( user ) ;
2025-04-26 23:15:54 +00:00
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
public async Task SwitchChannel ( User user , Channel chan , string password ) {
2025-05-03 02:49:51 +00:00
Channel ? oldChan = ChannelsUsers . GetUserLastChannel ( user ) ;
if ( oldChan ? . Id = = chan . Id ) {
2025-04-26 23:15:54 +00:00
await ForceChannel ( user ) ;
return ;
}
2025-04-27 01:54:46 +00:00
if ( ! user . Permissions . HasFlag ( UserPermissions . JoinAnyChannel ) & & chan . IsOwner ( user . UserId ) ) {
2025-04-26 23:15:54 +00:00
if ( chan . Rank > user . Rank ) {
await SendTo ( user , new CommandResponseS2CPacket ( RandomSnowflake . Next ( ) , LCR . CHANNEL_INSUFFICIENT_HIERARCHY , true , chan . Name ) ) ;
2025-04-26 22:28:41 +00:00
await ForceChannel ( user ) ;
2022-08-30 17:00:58 +02:00
return ;
}
2025-05-03 02:49:51 +00:00
if ( ! string . IsNullOrEmpty ( chan . Password ) & & chan . Password . SlowUtf8Equals ( password ) ) {
2025-04-26 23:15:54 +00:00
await SendTo ( user , new CommandResponseS2CPacket ( RandomSnowflake . Next ( ) , LCR . CHANNEL_INVALID_PASSWORD , true , chan . Name ) ) ;
await ForceChannel ( user ) ;
return ;
2022-08-30 17:00:58 +02:00
}
}
2025-05-03 02:49:51 +00:00
if ( oldChan is not null ) {
long leaveId = RandomSnowflake . Next ( ) ;
await SendTo ( oldChan , new UserChannelLeaveS2CPacket ( leaveId , user . UserId ) ) ;
await Messages . LogMessage ( leaveId , "chan:leave" , oldChan . Name , user . UserId , user . UserName , user . Colour , user . Rank , user . NickName , user . Permissions ) ;
ChannelsUsers . RemoveChannelUser ( oldChan , user ) ;
2022-08-30 17:00:58 +02:00
2025-05-03 02:49:51 +00:00
if ( oldChan . IsTemporary & & oldChan . IsOwner ( user . UserId ) )
await RemoveChannel ( oldChan ) ;
}
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
long joinId = RandomSnowflake . Next ( ) ;
2025-05-03 02:49:51 +00:00
await SendTo ( chan , new UserChannelJoinS2CPacket ( joinId , user . UserId , user . GetLegacyNameWithStatus ( ) , user . Colour , user . Rank , user . Permissions ) ) ;
await Messages . LogMessage ( joinId , "chan:join" , chan . Name , user . UserId , user . GetLegacyName ( ) , user . Colour , user . Rank , user . NickName , user . Permissions ) ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
await SendTo ( user , new ContextClearS2CPacket ( ContextClearS2CPacket . Mode . MessagesUsers ) ) ;
await SendTo ( user , new ContextUsersS2CPacket (
2025-05-03 02:49:51 +00:00
ChannelsUsers . GetChannelUsers ( chan ) . Except ( [ user ] ) . OrderByDescending ( u = > u . Rank )
2025-04-26 23:15:54 +00:00
. Select ( u = > new ContextUsersS2CPacket . Entry (
u . UserId ,
2025-05-03 02:49:51 +00:00
u . GetLegacyNameWithStatus ( ) ,
2025-04-26 23:15:54 +00:00
u . Colour ,
u . Rank ,
u . Permissions ,
true
) )
) ) ;
2022-08-30 17:00:58 +02:00
2025-04-27 01:54:46 +00:00
IEnumerable < Message > msgs = await Messages . GetMessages ( chan . Name ) ;
foreach ( Message msg in msgs )
2025-04-26 23:15:54 +00:00
await SendTo ( user , new ContextMessageS2CPacket ( msg ) ) ;
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
await ForceChannel ( user , chan ) ;
2025-05-03 02:49:51 +00:00
ChannelsUsers . AddChannelUser ( chan , user ) ;
2025-04-26 23:15:54 +00:00
}
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
public async Task Send ( S2CPacket packet ) {
2025-05-03 02:49:51 +00:00
foreach ( SockChatConnection conn in Sessions . GetConnections < SockChatConnection > ( ) )
await conn . Send ( packet ) ;
2025-04-26 23:15:54 +00:00
}
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
public async Task SendTo ( User user , S2CPacket packet ) {
2025-05-03 02:49:51 +00:00
foreach ( SockChatConnection conn in Sessions . GetConnections < SockChatConnection > ( user ) )
await conn . Send ( packet ) ;
2025-04-26 23:15:54 +00:00
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public async Task SendTo ( Channel channel , S2CPacket packet ) {
// might be faster to grab the users first and then cascade into that SendTo
2025-05-03 02:49:51 +00:00
IEnumerable < SockChatConnection > conns = Sessions . GetConnections < SockChatConnection > (
s = > ChannelsUsers . HasChannelUser ( channel , s . UserId )
) ;
foreach ( SockChatConnection conn in conns )
2025-04-26 23:15:54 +00:00
await conn . Send ( packet ) ;
}
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
public async Task SendToUserChannels ( User user , S2CPacket packet ) {
2025-05-03 02:49:51 +00:00
IEnumerable < Channel > chans = ChannelsUsers . GetUserChannels ( user ) ;
IEnumerable < SockChatConnection > conns = Sessions . GetConnections < SockChatConnection > (
s = > chans . Any ( c = > ChannelsUsers . HasChannelUser ( c . Id , s . UserId ) )
) ;
foreach ( SockChatConnection conn in conns )
2025-04-26 23:15:54 +00:00
await conn . Send ( packet ) ;
}
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
public async Task ForceChannel ( User user , Channel ? chan = null ) {
2025-05-03 02:49:51 +00:00
chan ? ? = ChannelsUsers . GetUserLastChannel ( user ) ? ? throw new ArgumentException ( "no channel???" ) ;
2025-04-26 23:15:54 +00:00
await SendTo ( user , new UserChannelForceJoinS2CPacket ( chan . Name ) ) ;
}
2023-02-10 07:07:59 +01:00
2025-04-27 02:53:56 +00:00
public async Task UpdateChannel (
Channel channel ,
bool? temporary = null ,
int? rank = null ,
string? password = null
) {
2025-05-03 02:49:51 +00:00
ChannelDiff diff = Channels . UpdateChannel (
2025-04-27 02:53:56 +00:00
channel ,
temporary : temporary ,
rank : rank ,
password : password
) ;
2023-02-10 07:07:59 +01:00
2025-04-26 23:15:54 +00:00
// TODO: 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
2025-05-03 02:49:51 +00:00
if ( diff . Changed )
foreach ( User user in Users . GetUsersOfMinimumRank ( channel . Rank ) )
await SendTo ( user , new ChannelUpdateS2CPacket ( channel . Name , channel . Name , channel . HasPassword , channel . IsTemporary ) ) ;
2025-04-26 23:15:54 +00:00
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
public async Task RemoveChannel ( Channel channel ) {
// Remove channel from the listing
2025-05-03 02:49:51 +00:00
Channels . RemoveChannel ( channel ) ;
2025-04-26 23:15:54 +00:00
// 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.
2025-05-03 02:49:51 +00:00
foreach ( User user in ChannelsUsers . GetChannelUsers ( channel ) )
await SwitchChannel ( user , Channels . GetDefaultChannel ( ) , string . Empty ) ;
2025-04-26 23:15:54 +00:00
// Broadcast deletion of channel
2025-05-03 02:49:51 +00:00
foreach ( User user in Users . GetUsersOfMinimumRank ( channel . Rank ) )
2025-04-26 23:15:54 +00:00
await SendTo ( user , new ChannelDeleteS2CPacket ( channel . Name ) ) ;
2025-05-03 02:49:51 +00:00
ChannelsUsers . RemoveChannel ( channel ) ;
2022-08-30 17:00:58 +02:00
}
}