2025-04-26 19:42:23 +00:00
using SharpChat.Events ;
2023-07-23 21:31:13 +00:00
using SharpChat.EventStorage ;
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 ;
2023-02-16 23:33:48 +01:00
using System.Net ;
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 {
public record ChannelUserAssoc ( string UserId , string ChannelName ) ;
2023-07-23 21:31:13 +00:00
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-26 23:15:54 +00:00
public SnowflakeGenerator SnowflakeGenerator { get ; } = new ( ) ;
public RandomSnowflake RandomSnowflake { get ; }
2023-02-19 23:27:08 +01:00
2025-04-26 23:15:54 +00:00
public HashSet < Channel > Channels { get ; } = [ ] ;
public HashSet < Connection > Connections { get ; } = [ ] ;
public HashSet < User > Users { get ; } = [ ] ;
public EventStorage . EventStorage Events { get ; }
public HashSet < ChannelUserAssoc > ChannelUsers { get ; } = [ ] ;
public Dictionary < string , RateLimiter > UserRateLimiters { get ; } = [ ] ;
public Dictionary < string , Channel > UserLastChannel { get ; } = [ ] ;
2023-02-19 23:27:08 +01:00
2025-04-26 23:15:54 +00:00
public Context ( EventStorage . EventStorage evtStore ) {
Events = evtStore ? ? throw new ArgumentNullException ( nameof ( evtStore ) ) ;
RandomSnowflake = new ( SnowflakeGenerator ) ;
}
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-04-26 23:15:54 +00:00
IEnumerable < User > users = Users . Where ( u = > uids . Any ( uid = > uid = = u . UserId ) ) ;
User ? target = users . FirstOrDefault ( u = > u . UserId ! = mce . SenderId ) ;
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 ,
mce . SenderId = = user . UserId ? $"{target.LegacyName} {mce.MessageText}" : mce . MessageText ,
mce . IsAction ,
true
) ) ;
} else {
Channel ? channel = Channels . FirstOrDefault ( c = > c . NameEquals ( mce . ChannelName ) ) ;
if ( channel is not null )
await SendTo ( channel , new ChatMessageAddS2CPacket (
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 ;
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 ( ) {
foreach ( Connection conn in Connections )
if ( ! conn . IsDisposed & & conn . HasTimedOut ) {
conn . Dispose ( ) ;
Logger . Write ( $"Nuked connection {conn.Id} associated with {conn.User}." ) ;
2023-02-22 01:28:53 +01:00
}
2025-04-26 23:15:54 +00:00
Connections . RemoveWhere ( conn = > conn . IsDisposed ) ;
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
foreach ( User user in Users )
if ( ! Connections . Any ( conn = > conn . User = = user ) ) {
Logger . Write ( $"Timing out {user} (no more connections)." ) ;
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 bool IsInChannel ( User user , Channel channel ) {
return ChannelUsers . Contains ( new ChannelUserAssoc ( user . UserId , channel . Name ) ) ;
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public string [ ] GetUserChannelNames ( User user ) {
return [ . . ChannelUsers . Where ( cu = > cu . UserId = = user . UserId ) . Select ( cu = > cu . ChannelName ) ] ;
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public Channel [ ] GetUserChannels ( User user ) {
string [ ] names = GetUserChannelNames ( user ) ;
return [ . . Channels . Where ( c = > names . Any ( n = > c . NameEquals ( n ) ) ) ] ;
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public string [ ] GetChannelUserIds ( Channel channel ) {
return [ . . ChannelUsers . Where ( cu = > channel . NameEquals ( cu . ChannelName ) ) . Select ( cu = > cu . UserId ) ] ;
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public User [ ] GetChannelUsers ( Channel channel ) {
string [ ] ids = GetChannelUserIds ( channel ) ;
return [ . . Users . Where ( u = > ids . Contains ( u . UserId ) ) ] ;
}
2023-11-07 14:49:12 +00: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
) {
ArgumentNullException . ThrowIfNull ( user ) ;
bool hasChanged = false ;
string previousName = string . Empty ;
if ( userName ! = null & & ! user . UserName . Equals ( userName ) ) {
user . UserName = userName ;
hasChanged = true ;
2023-02-22 01:28:53 +01:00
}
2025-04-26 23:15:54 +00:00
if ( nickName ! = null & & ! user . NickName . Equals ( nickName ) ) {
if ( ! silent )
previousName = user . LegacyName ;
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
user . NickName = nickName ;
hasChanged = true ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( colour . HasValue & & user . Colour ! = colour . Value ) {
user . Colour = colour . Value ;
hasChanged = true ;
2022-08-30 17:00:58 +02:00
}
2025-04-26 23:15:54 +00:00
if ( status . HasValue & & user . Status ! = status . Value ) {
user . Status = status . Value ;
hasChanged = true ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( statusText ! = null & & ! user . StatusText . Equals ( statusText ) ) {
user . StatusText = statusText ;
hasChanged = true ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( rank ! = null & & user . Rank ! = rank ) {
user . Rank = ( int ) rank ;
hasChanged = true ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( perms . HasValue & & user . Permissions ! = perms ) {
user . Permissions = perms . Value ;
hasChanged = true ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( hasChanged ) {
if ( ! string . IsNullOrWhiteSpace ( previousName ) )
await SendToUserChannels ( user , new CommandResponseS2CPacket ( RandomSnowflake . Next ( ) , LCR . NICKNAME_CHANGE , false , previousName , user . LegacyNameWithStatus ) ) ;
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
await SendToUserChannels ( user , new UserUpdateS2CPacket ( user . UserId , user . LegacyNameWithStatus , user . Colour , user . Rank , user . Permissions ) ) ;
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-04-26 23:15:54 +00:00
foreach ( Connection conn in Connections )
if ( conn . User = = user )
conn . Dispose ( ) ;
Connections . RemoveWhere ( conn = > conn . IsDisposed ) ;
2022-08-30 17:00:58 +02:00
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 HandleJoin ( User user , Channel chan , Connection conn , int maxMsgLength ) {
if ( ! IsInChannel ( user , chan ) ) {
long msgId = RandomSnowflake . Next ( ) ;
await SendTo ( chan , new UserConnectS2CPacket ( msgId , DateTimeOffset . Now , user . UserId , user . LegacyNameWithStatus , user . Colour , user . Rank , user . Permissions ) ) ;
Events . AddEvent ( msgId , "user:connect" , chan . Name , user . UserId , user . UserName , user . Colour , user . Rank , user . NickName , user . Permissions , null , StoredEventFlags . Log ) ;
}
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
await conn . Send ( new AuthSuccessS2CPacket (
user . UserId ,
user . LegacyNameWithStatus ,
user . Colour ,
user . Rank ,
user . Permissions ,
chan . Name ,
maxMsgLength
) ) ;
await conn . Send ( new ContextUsersS2CPacket (
GetChannelUsers ( chan ) . Except ( [ user ] ) . OrderByDescending ( u = > u . Rank )
. Select ( u = > new ContextUsersS2CPacket . Entry (
u . UserId ,
u . LegacyNameWithStatus ,
u . Colour ,
u . Rank ,
u . Permissions ,
true
) )
) ) ;
foreach ( StoredEventInfo msg in Events . GetChannelEventLog ( chan . Name ) )
await conn . Send ( new ContextMessageS2CPacket ( msg ) ) ;
await conn . Send ( new ContextChannelsS2CPacket (
Channels . Where ( c = > c . Rank < = user . Rank )
. Select ( c = > new ContextChannelsS2CPacket . Entry ( c . Name , c . HasPassword , c . IsTemporary ) )
) ) ;
Users . Add ( user ) ;
ChannelUsers . Add ( new ChannelUserAssoc ( user . UserId , chan . Name ) ) ;
UserLastChannel [ user . UserId ] = chan ;
}
public async Task HandleDisconnect ( User user , UserDisconnectS2CPacket . Reason reason = UserDisconnectS2CPacket . Reason . Leave ) {
await UpdateUser ( user , status : UserStatus . Offline ) ;
Users . Remove ( user ) ;
UserLastChannel . Remove ( user . UserId ) ;
Channel [ ] channels = GetUserChannels ( user ) ;
foreach ( Channel chan in channels ) {
ChannelUsers . Remove ( new ChannelUserAssoc ( user . UserId , chan . Name ) ) ;
long msgId = RandomSnowflake . Next ( ) ;
await SendTo ( chan , new UserDisconnectS2CPacket ( msgId , DateTimeOffset . Now , user . UserId , user . LegacyNameWithStatus , reason ) ) ;
Events . AddEvent ( msgId , "user:disconnect" , chan . Name , user . UserId , user . UserName , user . Colour , user . Rank , user . NickName , user . Permissions , new { reason = ( int ) reason } , StoredEventFlags . Log ) ;
if ( chan . IsTemporary & & chan . IsOwner ( user ) )
await RemoveChannel ( chan ) ;
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 SwitchChannel ( User user , Channel chan , string password ) {
if ( UserLastChannel . TryGetValue ( user . UserId , out Channel ? ulc ) & & chan = = ulc ) {
await ForceChannel ( user ) ;
return ;
}
if ( ! user . Can ( UserPermissions . JoinAnyChannel ) & & chan . IsOwner ( user ) ) {
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-04-26 23:15:54 +00:00
if ( ! string . IsNullOrEmpty ( chan . Password ) & & chan . Password ! = password ) {
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-04-26 23:15:54 +00:00
await ForceChannelSwitch ( user , chan ) ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
public async Task ForceChannelSwitch ( User user , Channel chan ) {
if ( ! Channels . Contains ( chan ) )
return ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
Channel oldChan = UserLastChannel [ user . UserId ] ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
long leaveId = RandomSnowflake . Next ( ) ;
await SendTo ( oldChan , new UserChannelLeaveS2CPacket ( leaveId , user . UserId ) ) ;
Events . AddEvent ( leaveId , "chan:leave" , oldChan . Name , user . UserId , user . UserName , user . Colour , user . Rank , user . NickName , user . Permissions , null , StoredEventFlags . Log ) ;
2023-02-17 20:02:35 +01:00
2025-04-26 23:15:54 +00:00
long joinId = RandomSnowflake . Next ( ) ;
await SendTo ( chan , new UserChannelJoinS2CPacket ( joinId , user . UserId , user . LegacyNameWithStatus , user . Colour , user . Rank , user . Permissions ) ) ;
Events . AddEvent ( joinId , "chan:join" , chan . Name , user . UserId , user . LegacyName , user . Colour , user . Rank , user . NickName , user . Permissions , null , StoredEventFlags . Log ) ;
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 (
GetChannelUsers ( chan ) . Except ( [ user ] ) . OrderByDescending ( u = > u . Rank )
. Select ( u = > new ContextUsersS2CPacket . Entry (
u . UserId ,
u . LegacyNameWithStatus ,
u . Colour ,
u . Rank ,
u . Permissions ,
true
) )
) ) ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
foreach ( StoredEventInfo msg in Events . GetChannelEventLog ( chan . Name ) )
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 ) ;
ChannelUsers . Remove ( new ChannelUserAssoc ( user . UserId , oldChan . Name ) ) ;
ChannelUsers . Add ( new ChannelUserAssoc ( user . UserId , chan . Name ) ) ;
UserLastChannel [ user . UserId ] = chan ;
if ( oldChan . IsTemporary & & oldChan . IsOwner ( user ) )
await RemoveChannel ( oldChan ) ;
}
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
public async Task Send ( S2CPacket packet ) {
foreach ( Connection conn in Connections )
if ( conn . IsAlive & & conn . User is not null )
2025-04-26 22:28:41 +00:00
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 ) {
foreach ( Connection conn in Connections )
if ( conn . IsAlive & & conn . User = = user )
2025-04-26 22:28:41 +00:00
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
IEnumerable < Connection > conns = Connections . Where ( c = > c . IsAlive & & c . User is not null & & IsInChannel ( c . User , channel ) ) ;
foreach ( Connection conn in conns )
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 ) {
IEnumerable < Channel > chans = Channels . Where ( c = > IsInChannel ( user , c ) ) ;
IEnumerable < Connection > conns = Connections . Where ( conn = > conn . IsAlive & & conn . User is not null & & ChannelUsers . Any ( cu = > cu . UserId = = conn . User . UserId & & chans . Any ( chan = > chan . NameEquals ( cu . ChannelName ) ) ) ) ;
foreach ( Connection conn in conns )
await conn . Send ( packet ) ;
}
2023-02-16 23:33:48 +01:00
2025-04-26 23:15:54 +00:00
public IPAddress [ ] GetRemoteAddresses ( User user ) {
return [ . . Connections . Where ( c = > c . IsAlive & & c . User = = user ) . Select ( c = > c . RemoteAddress ) . Distinct ( ) ] ;
}
2023-02-22 01:28:53 +01:00
2025-04-26 23:15:54 +00:00
public async Task ForceChannel ( User user , Channel ? chan = null ) {
ArgumentNullException . ThrowIfNull ( user ) ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( chan = = null & & ! UserLastChannel . TryGetValue ( user . UserId , out chan ) )
throw new ArgumentException ( "no channel???" ) ;
2022-08-30 17:00:58 +02:00
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-26 23:15:54 +00:00
public async Task UpdateChannel ( Channel channel , bool? temporary = null , int? hierarchy = null , string? password = null ) {
ArgumentNullException . ThrowIfNull ( channel ) ;
if ( ! Channels . Contains ( channel ) )
throw new ArgumentException ( "Provided channel is not registered with this manager." , nameof ( channel ) ) ;
2023-02-10 07:07:59 +01:00
2025-04-26 23:15:54 +00:00
if ( temporary . HasValue )
channel . IsTemporary = temporary . Value ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( hierarchy . HasValue )
channel . Rank = hierarchy . Value ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
if ( password ! = null )
channel . 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
foreach ( User user in Users . Where ( u = > u . Rank > = channel . Rank ) )
await SendTo ( user , new ChannelUpdateS2CPacket ( channel . Name , channel . Name , channel . HasPassword , channel . IsTemporary ) ) ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
public async Task RemoveChannel ( Channel channel ) {
if ( channel = = null | | Channels . Count < 1 )
return ;
2023-02-10 07:07:59 +01:00
2025-04-26 23:15:54 +00:00
Channel ? defaultChannel = Channels . FirstOrDefault ( ) ;
if ( defaultChannel is null )
return ;
2023-02-10 07:07:59 +01:00
2025-04-26 23:15:54 +00:00
// 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 ( User user in GetChannelUsers ( channel ) )
await SwitchChannel ( user , defaultChannel , string . Empty ) ;
// Broadcast deletion of channel
foreach ( User user in Users . Where ( u = > u . Rank > = channel . Rank ) )
await SendTo ( user , new ChannelDeleteS2CPacket ( channel . Name ) ) ;
2022-08-30 17:00:58 +02:00
}
}