diff --git a/SharpChat.SockChat/Commands/ChannelCreateCommand.cs b/SharpChat.SockChat/Commands/ChannelCreateCommand.cs index 93102c6..6114785 100644 --- a/SharpChat.SockChat/Commands/ChannelCreateCommand.cs +++ b/SharpChat.SockChat/Commands/ChannelCreateCommand.cs @@ -1,4 +1,6 @@ -using SharpChat.SockChat.PacketsS2C; +using SharpChat.Events; +using SharpChat.SockChat.PacketsS2C; +using System; using System.Linq; namespace SharpChat.SockChat.Commands { @@ -31,35 +33,34 @@ namespace SharpChat.SockChat.Commands { return; } - string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0)); + string channelName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0)); - if(!SockChatUtility.CheckChannelName(createChanName)) { + if(!SockChatUtility.CheckChannelName(channelName)) { ctx.Chat.SendTo(ctx.User, new ChannelNameFormatErrorS2CPacket()); return; } - if(ctx.Chat.Channels.Get(createChanName, SockChatUtility.SanitiseChannelName) != null) { - ctx.Chat.SendTo(ctx.User, new ChannelNameInUseErrorS2CPacket(createChanName)); + if(ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName) != null) { + ctx.Chat.SendTo(ctx.User, new ChannelNameInUseErrorS2CPacket(channelName)); return; } - ChannelInfo createChan = new( - createChanName, - isTemporary: !ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPermanent), - rank: createChanHierarchy, - ownerId: ctx.User.UserId + ctx.Chat.Events.Dispatch( + "chan:add", + channelName, + ctx.User, + new ChannelAddEventData( + !ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPermanent), + createChanHierarchy, + string.Empty + ) ); - ctx.Chat.Channels.Add(createChan, sanitiseName: SockChatUtility.SanitiseChannelName); - foreach(UserInfo ccu in ctx.Chat.Users.GetMany(minRank: ctx.Channel.Rank)) - ctx.Chat.SendTo(ccu, new ChannelCreateS2CPacket( - ctx.Channel.Name, - ctx.Channel.HasPassword, - ctx.Channel.IsTemporary - )); + DateTimeOffset now = DateTimeOffset.UtcNow; + ctx.Chat.Events.Dispatch("chan:leave", now, ctx.Channel, ctx.User); + ctx.Chat.Events.Dispatch("chan:join", now, channelName, ctx.User); - ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password); - ctx.Chat.SendTo(ctx.User, new ChannelCreateResponseS2CPacket(createChan.Name)); + ctx.Chat.SendTo(ctx.User, new ChannelCreateResponseS2CPacket(channelName)); } } } diff --git a/SharpChat.SockChat/Commands/ChannelDeleteCommand.cs b/SharpChat.SockChat/Commands/ChannelDeleteCommand.cs index 443fef0..7372018 100644 --- a/SharpChat.SockChat/Commands/ChannelDeleteCommand.cs +++ b/SharpChat.SockChat/Commands/ChannelDeleteCommand.cs @@ -24,12 +24,12 @@ namespace SharpChat.SockChat.Commands { return; } - if(!ctx.User.Permissions.HasFlag(UserPermissions.DeleteChannel) || !delChan.IsOwner(ctx.User)) { + if(!ctx.User.Permissions.HasFlag(UserPermissions.DeleteChannel) || delChan.OwnerId != ctx.User.UserId) { ctx.Chat.SendTo(ctx.User, new ChannelDeleteNotAllowedErrorS2CPacket(delChan.Name)); return; } - ctx.Chat.RemoveChannel(delChan); + ctx.Chat.Events.Dispatch("chan:delete", delChan, ctx.User); ctx.Chat.SendTo(ctx.User, new ChannelDeleteResponseS2CPacket(delChan.Name)); } } diff --git a/SharpChat.SockChat/Commands/ChannelJoinCommand.cs b/SharpChat.SockChat/Commands/ChannelJoinCommand.cs index 9d0b33d..7fc73c7 100644 --- a/SharpChat.SockChat/Commands/ChannelJoinCommand.cs +++ b/SharpChat.SockChat/Commands/ChannelJoinCommand.cs @@ -1,4 +1,5 @@ using SharpChat.SockChat.PacketsS2C; +using System; using System.Linq; namespace SharpChat.SockChat.Commands { @@ -8,16 +9,40 @@ namespace SharpChat.SockChat.Commands { } public void Dispatch(SockChatClientCommandContext ctx) { - string joinChanStr = ctx.Args.FirstOrDefault() ?? string.Empty; - ChannelInfo? joinChan = ctx.Chat.Channels.Get(joinChanStr, SockChatUtility.SanitiseChannelName); + string channelName = ctx.Args.FirstOrDefault() ?? string.Empty; + string password = string.Join(' ', ctx.Args.Skip(1)); - if(joinChan == null) { - ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(joinChanStr)); - ctx.Chat.ForceChannel(ctx.User); + ChannelInfo? channelInfo = ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName); + if(channelInfo == null) { + ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(channelName)); + ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name)); return; } - ctx.Chat.SwitchChannel(ctx.User, joinChan, string.Join(' ', ctx.Args.Skip(1))); + if(channelInfo.Name.Equals(ctx.Channel.Name, StringComparison.InvariantCultureIgnoreCase)) { + // this is where the elusive commented out "samechan" error would go! + // https://patchii.net/sockchat/sockchat/src/commit/6c2111fb4b0241f9ef31060b0f86e7176664f572/server/lib/context.php#L61 + ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name)); + return; + } + + if(!ctx.User.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && channelInfo.OwnerId != ctx.User.UserId) { + if(channelInfo.Rank > ctx.User.Rank) { + ctx.Chat.SendTo(ctx.User, new ChannelRankTooLowErrorS2CPacket(channelInfo.Name)); + ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name)); + return; + } + + if(!string.IsNullOrEmpty(channelInfo.Password) && channelInfo.Password.Equals(password)) { + ctx.Chat.SendTo(ctx.User, new ChannelPasswordWrongErrorS2CPacket(channelInfo.Name)); + ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name)); + return; + } + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + ctx.Chat.Events.Dispatch("chan:leave", now, ctx.Channel, ctx.User); + ctx.Chat.Events.Dispatch("chan:join", now, channelInfo, ctx.User); } } } diff --git a/SharpChat.SockChat/Commands/ChannelPasswordCommand.cs b/SharpChat.SockChat/Commands/ChannelPasswordCommand.cs index b8c66ff..ddc6e26 100644 --- a/SharpChat.SockChat/Commands/ChannelPasswordCommand.cs +++ b/SharpChat.SockChat/Commands/ChannelPasswordCommand.cs @@ -1,4 +1,5 @@ -using SharpChat.SockChat.PacketsS2C; +using SharpChat.Events; +using SharpChat.SockChat.PacketsS2C; namespace SharpChat.SockChat.Commands { public class ChannelPasswordCommand : ISockChatClientCommand { @@ -8,7 +9,7 @@ namespace SharpChat.SockChat.Commands { } public void Dispatch(SockChatClientCommandContext ctx) { - if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPassword) || !ctx.Channel.IsOwner(ctx.User)) { + if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPassword) || ctx.Channel.OwnerId != ctx.User.UserId) { ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name)); return; } @@ -18,7 +19,7 @@ namespace SharpChat.SockChat.Commands { if(string.IsNullOrWhiteSpace(chanPass)) chanPass = string.Empty; - ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass); + ctx.Chat.Events.Dispatch("chan:update", ctx.Channel.Name, ctx.User, new ChannelUpdateEventData(password: chanPass)); ctx.Chat.SendTo(ctx.User, new ChannelPasswordChangedResponseS2CPacket()); } } diff --git a/SharpChat.SockChat/Commands/ChannelRankCommand.cs b/SharpChat.SockChat/Commands/ChannelRankCommand.cs index 95eed6d..bf6cbc2 100644 --- a/SharpChat.SockChat/Commands/ChannelRankCommand.cs +++ b/SharpChat.SockChat/Commands/ChannelRankCommand.cs @@ -1,4 +1,5 @@ -using SharpChat.SockChat.PacketsS2C; +using SharpChat.Events; +using SharpChat.SockChat.PacketsS2C; using System.Linq; namespace SharpChat.SockChat.Commands { @@ -10,17 +11,17 @@ namespace SharpChat.SockChat.Commands { } public void Dispatch(SockChatClientCommandContext ctx) { - if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelHierarchy) || !ctx.Channel.IsOwner(ctx.User)) { + if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelHierarchy) || ctx.Channel.OwnerId != ctx.User.UserId) { ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name)); return; } - if(!ctx.Args.Any() || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) { + if(!ctx.Args.Any() || !int.TryParse(ctx.Args.First(), out int chanMinRank) || chanMinRank > ctx.User.Rank) { ctx.Chat.SendTo(ctx.User, new ChannelRankTooHighErrorS2CPacket()); return; } - ctx.Chat.UpdateChannel(ctx.Channel, minRank: chanHierarchy); + ctx.Chat.Events.Dispatch("chan:update", ctx.Channel.Name, ctx.User, new ChannelUpdateEventData(minRank: chanMinRank)); ctx.Chat.SendTo(ctx.User, new ChannelRankChangedResponseS2CPacket()); } } diff --git a/SharpChat.SockChat/Commands/KickBanCommand.cs b/SharpChat.SockChat/Commands/KickBanCommand.cs index c2418e9..d8a672f 100644 --- a/SharpChat.SockChat/Commands/KickBanCommand.cs +++ b/SharpChat.SockChat/Commands/KickBanCommand.cs @@ -54,7 +54,7 @@ namespace SharpChat.SockChat.Commands { } if(duration <= TimeSpan.Zero) { - ctx.Chat.Events.Dispatch("user:kickban", banUser, new UserKickBanEventData(UserDisconnectReason.Kicked, TimeSpan.Zero)); + ctx.Chat.Events.Dispatch("user:kickban", banUser, UserKickBanEventData.OfDuration(UserDisconnectReason.Kicked, TimeSpan.Zero)); return; } @@ -79,7 +79,7 @@ namespace SharpChat.SockChat.Commands { duration, banReason ); - ctx.Chat.Events.Dispatch("user:kickban", banUser, new UserKickBanEventData(UserDisconnectReason.Kicked, duration)); + ctx.Chat.Events.Dispatch("user:kickban", banUser, UserKickBanEventData.OfDuration(UserDisconnectReason.Kicked, duration)); }).Wait(); } } diff --git a/SharpChat.SockChat/Commands/WhoCommand.cs b/SharpChat.SockChat/Commands/WhoCommand.cs index 157736f..c1803a2 100644 --- a/SharpChat.SockChat/Commands/WhoCommand.cs +++ b/SharpChat.SockChat/Commands/WhoCommand.cs @@ -30,9 +30,13 @@ namespace SharpChat.SockChat.Commands { return; } + UserInfo[] userInfos = ctx.Chat.Users.GetMany( + ctx.Chat.ChannelsUsers.GetChannelUserIds(channel) + ); + ctx.Chat.SendTo(ctx.User, new WhoChannelResponseS2CPacket( channel.Name, - ctx.Chat.GetChannelUsers(channel).Select(user => SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user))).ToArray(), + userInfos.Select(user => SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user))).ToArray(), SockChatUtility.GetUserName(ctx.User, ctx.Chat.UserStatuses.Get(ctx.User)) )); } diff --git a/SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs b/SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs index 0027d04..3f90b5f 100644 --- a/SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs +++ b/SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs @@ -15,6 +15,7 @@ namespace SharpChat.SockChat.PacketsC2S { private readonly DateTimeOffset Started; private readonly MisuzuClient Misuzu; private readonly ChannelInfo DefaultChannel; + private readonly CachedValue MOTDHeaderFormat; private readonly CachedValue MaxMessageLength; private readonly CachedValue MaxConnections; @@ -22,12 +23,14 @@ namespace SharpChat.SockChat.PacketsC2S { DateTimeOffset started, MisuzuClient msz, ChannelInfo? defaultChannel, + CachedValue motdHeaderFormat, CachedValue maxMsgLength, CachedValue maxConns ) { Started = started; Misuzu = msz; DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel)); + MOTDHeaderFormat = motdHeaderFormat; MaxMessageLength = maxMsgLength; MaxConnections = maxConns; } @@ -118,19 +121,35 @@ namespace SharpChat.SockChat.PacketsC2S { return; } + if(ctx.Chat.Connections.GetCountForUser(fai.UserId) >= MaxConnections) { + ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.MaxSessions)); + ctx.Connection.Close(1000); + return; + } + await ctx.Chat.ContextAccess.WaitAsync(); try { UserInfo? user = ctx.Chat.Users.Get(fai.UserId); if(user == null) { - user = new UserInfo( + ctx.Chat.Events.Dispatch( + "user:add", fai.UserId, fai.UserName ?? string.Empty, fai.Colour, fai.Rank, + string.Empty, fai.Permissions, - isSuper: fai.IsSuper + new UserAddEventData(fai.IsSuper) ); + + user = ctx.Chat.Users.Get(fai.UserId); + if(user == null) { + Logger.Write($"<{ctx.Connection.RemoteEndPoint}> User didn't get added."); + ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.Null)); + ctx.Connection.Close(1000); + return; + } } else { string? updName = !user.UserName.Equals(fai.UserName) ? fai.UserName : null; int? updColour = (updColour = fai.Colour.ToMisuzu()) != user.Colour.ToMisuzu() ? updColour : null; @@ -152,16 +171,11 @@ namespace SharpChat.SockChat.PacketsC2S { ); } - // Enforce a maximum amount of connections per user - if(ctx.Chat.Connections.GetCountForUser(user) >= MaxConnections) { - ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.MaxSessions)); - ctx.Connection.Close(1000); - return; - } - ctx.Connection.BumpPing(); ctx.Chat.Connections.SetUser(ctx.Connection, user); - ctx.Connection.Send(new MOTDS2CPacket(Started, $"Welcome to Flashii Chat, {user.UserName}!")); + + if(!string.IsNullOrWhiteSpace(MOTDHeaderFormat.Value)) + ctx.Connection.Send(new MOTDS2CPacket(Started, string.Format(MOTDHeaderFormat.Value, user.UserName))); if(File.Exists(MOTD_FILE)) { IEnumerable lines = File.ReadAllLines(MOTD_FILE).Where(x => !string.IsNullOrWhiteSpace(x)); @@ -171,7 +185,49 @@ namespace SharpChat.SockChat.PacketsC2S { ctx.Connection.Send(new MOTDS2CPacket(File.GetLastWriteTimeUtc(MOTD_FILE), line)); } - ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); + ctx.Connection.Send(new AuthSuccessS2CPacket( + user.UserId, + SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user)), + user.Colour, + user.Rank, + user.Permissions, + DefaultChannel.Name, + MaxMessageLength + )); + + UserInfo[] chanUsers = ctx.Chat.Users.GetMany( + ctx.Chat.ChannelsUsers.GetChannelUserIds(DefaultChannel) + ); + List chanUserEntries = new(); + foreach(UserInfo chanUserInfo in chanUsers) + if(chanUserInfo.UserId != user.UserId) + chanUserEntries.Add(new( + chanUserInfo.UserId, + SockChatUtility.GetUserName(chanUserInfo, ctx.Chat.UserStatuses.Get(chanUserInfo)), + chanUserInfo.Colour, + chanUserInfo.Rank, + chanUserInfo.Permissions, + true + )); + ctx.Connection.Send(new UsersPopulateS2CPacket(chanUserEntries.ToArray())); + + ctx.Chat.Events.Dispatch( + "user:connect", + DefaultChannel, + user, + new UserConnectEventData(!ctx.Chat.ChannelsUsers.Has(DefaultChannel, user)) + ); + ctx.Chat.HandleChannelEventLog(DefaultChannel.Name, p => ctx.Connection.Send(p)); + + ChannelInfo[] chans = ctx.Chat.Channels.GetMany(isPublic: true, minRank: user.Rank); + List chanEntries = new(); + foreach(ChannelInfo chanInfo in chans) + chanEntries.Add(new( + chanInfo.Name, + chanInfo.HasPassword, + chanInfo.IsTemporary + )); + ctx.Connection.Send(new ChannelsPopulateS2CPacket(chanEntries.ToArray())); } finally { ctx.Chat.ContextAccess.Release(); } diff --git a/SharpChat.SockChat/SockChatContext.cs b/SharpChat.SockChat/SockChatContext.cs index 20ae7d3..8f202ea 100644 --- a/SharpChat.SockChat/SockChatContext.cs +++ b/SharpChat.SockChat/SockChatContext.cs @@ -27,8 +27,6 @@ namespace SharpChat { } 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); UserStatusInfo userStatusInfo = UserStatuses.Get(info.SenderId); if(!string.IsNullOrWhiteSpace(info.ChannelName)) @@ -37,7 +35,27 @@ namespace SharpChat { // TODO: should user:connect and user:disconnect be channel agnostic? switch(info.Type) { + case "user:add": + Users.Add(new UserInfo( + info.SenderId, + info.SenderName, + info.SenderColour, + info.SenderRank, + info.SenderPerms, + info.SenderNickName, + info.Data is UserAddEventData uaData && uaData.IsSuper + )); + break; + + case "user:delete": + UserStatuses.Clear(info.SenderId); + Users.Remove(info.SenderId); + break; + case "user:connect": + if(info.Data is not UserConnectEventData ucData || !ucData.Notify) + break; + SendTo(info.ChannelName, new UserConnectS2CPacket( info.Id, info.Created, @@ -47,14 +65,10 @@ namespace SharpChat { info.SenderRank, info.SenderPerms )); + ChannelsUsers.Join(info.ChannelName, info.SenderId); break; case "user:disconnect": - if(userInfo != null) - Events.Dispatch("user:status", userInfo, new UserStatusUpdateEventData(UserStatus.Offline)); - UserStatuses.Clear(info.SenderId); - Users.Remove(info.SenderId); - ChannelInfo[] channels = Channels.GetMany(ChannelsUsers.GetUserChannelNames(info.SenderId)); ChannelsUsers.DeleteUser(info.SenderId); @@ -68,8 +82,8 @@ namespace SharpChat { ); foreach(ChannelInfo chan in channels) { - if(chan.IsTemporary && chan.IsOwner(info.SenderId)) - RemoveChannel(chan); + if(chan.IsTemporary && chan.OwnerId == info.SenderId) + Events.Dispatch("chan:delete", chan.Name, info); else SendTo(chan, udPacket); } @@ -100,42 +114,46 @@ namespace SharpChat { break; case "user:update": - if(info.Data is not UserUpdateEventData userUpdate || userInfo is null) + if(info.Data is not UserUpdateEventData userUpdate) + break; + + UserInfo? uuUserInfo = Users.Get(info.SenderId); + if(uuUserInfo is null) break; bool uuHasChanged = false; string? uuPrevName = null; - if(userUpdate.Name != null && !userUpdate.Name.Equals(userInfo.UserName)) { - userInfo.UserName = userUpdate.Name; + if(userUpdate.Name != null && !userUpdate.Name.Equals(uuUserInfo.UserName)) { + uuUserInfo.UserName = userUpdate.Name; uuHasChanged = true; } - if(userUpdate.NickName != null && !userUpdate.NickName.Equals(userInfo.NickName)) { + if(userUpdate.NickName != null && !userUpdate.NickName.Equals(uuUserInfo.NickName)) { if(userUpdate.Notify) - uuPrevName = string.IsNullOrWhiteSpace(userInfo.NickName) ? userInfo.UserName : userInfo.NickName; + uuPrevName = string.IsNullOrWhiteSpace(uuUserInfo.NickName) ? uuUserInfo.UserName : uuUserInfo.NickName; - userInfo.NickName = userUpdate.NickName; + uuUserInfo.NickName = userUpdate.NickName; uuHasChanged = true; } - if(userUpdate.Colour.HasValue && userUpdate.Colour != userInfo.Colour.ToMisuzu()) { - userInfo.Colour = Colour.FromMisuzu(userUpdate.Colour.Value); + if(userUpdate.Colour.HasValue && userUpdate.Colour != uuUserInfo.Colour.ToMisuzu()) { + uuUserInfo.Colour = Colour.FromMisuzu(userUpdate.Colour.Value); uuHasChanged = true; } - if(userUpdate.Rank != null && userUpdate.Rank != userInfo.Rank) { - userInfo.Rank = userUpdate.Rank.Value; + if(userUpdate.Rank != null && userUpdate.Rank != uuUserInfo.Rank) { + uuUserInfo.Rank = userUpdate.Rank.Value; uuHasChanged = true; } - if(userUpdate.Perms.HasValue && userUpdate.Perms != userInfo.Permissions) { - userInfo.Permissions = userUpdate.Perms.Value; + if(userUpdate.Perms.HasValue && userUpdate.Perms != uuUserInfo.Permissions) { + uuUserInfo.Permissions = userUpdate.Perms.Value; uuHasChanged = true; } - if(userUpdate.IsSuper.HasValue && userUpdate.IsSuper != userInfo.IsSuper) - userInfo.IsSuper = userUpdate.IsSuper.Value; + if(userUpdate.IsSuper.HasValue && userUpdate.IsSuper != uuUserInfo.IsSuper) + uuUserInfo.IsSuper = userUpdate.IsSuper.Value; if(uuHasChanged) { if(uuPrevName != null) @@ -143,15 +161,15 @@ namespace SharpChat { info.Id, info.Created, string.IsNullOrWhiteSpace(info.SenderNickName) ? uuPrevName : $"~{info.SenderNickName}", - SockChatUtility.GetUserName(userInfo, userStatusInfo) + SockChatUtility.GetUserName(uuUserInfo, userStatusInfo) )); SendToUserChannels(info.SenderId, new UserUpdateS2CPacket( - userInfo.UserId, - SockChatUtility.GetUserName(userInfo, userStatusInfo), - userInfo.Colour, - userInfo.Rank, - userInfo.Permissions + uuUserInfo.UserId, + SockChatUtility.GetUserName(uuUserInfo, userStatusInfo), + uuUserInfo.Colour, + uuUserInfo.Rank, + uuUserInfo.Permissions )); } break; @@ -189,17 +207,106 @@ namespace SharpChat { break; case "chan:join": - SendTo(info.ChannelName, new UserChannelJoinS2CPacket( - info.SenderId, - SockChatUtility.GetUserName(info, userStatusInfo), - info.SenderColour, - info.SenderRank, - info.SenderPerms - )); + HandleUserChannelJoin( + info.ChannelName, + new UserInfo(info), // kinda stinky + userStatusInfo + ); break; case "chan:leave": + ChannelsUsers.Leave(info.ChannelName, info.SenderId); SendTo(info.ChannelName, new UserChannelLeaveS2CPacket(info.SenderId)); + + ChannelInfo? clChannelInfo = Channels.Get(info.ChannelName); + if(clChannelInfo?.IsTemporary == true && clChannelInfo.OwnerId == info.SenderId) + Events.Dispatch("chan:delete", clChannelInfo.Name, info); + break; + + case "chan:add": + if(info.Data is not ChannelAddEventData caData) + break; + + ChannelInfo caChannelInfo = new( + info.ChannelName, + caData.Password, + caData.IsTemporary, + caData.MinRank, + info.SenderId + ); + + Channels.Add(caChannelInfo); + foreach(UserInfo ccu in Users.GetMany(minRank: caChannelInfo.Rank)) + SendTo(ccu, new ChannelCreateS2CPacket( + caChannelInfo.Name, + caChannelInfo.HasPassword, + caChannelInfo.IsTemporary + )); + break; + + case "chan:update": + if(info.Data is not ChannelUpdateEventData cuData) + break; + + ChannelInfo? cuChannelInfo = Channels.Get(info.ChannelName); + if(cuChannelInfo is null) + break; + + string cuChannelName = cuChannelInfo.Name; + + if(!string.IsNullOrEmpty(cuData.Name)) + cuChannelInfo.Name = cuData.Name; + + if(cuData.MinRank.HasValue) + cuChannelInfo.Rank = cuData.MinRank.Value; + + if(cuData.Password != null) // this should probably be hashed + cuChannelInfo.Password = cuData.Password; + + if(cuData.IsTemporary.HasValue) + cuChannelInfo.IsTemporary = cuData.IsTemporary.Value; + + bool nameChanged = !cuChannelName.Equals(cuChannelInfo.Name, StringComparison.InvariantCultureIgnoreCase); + if(nameChanged) + ChannelsUsers.RenameChannel(cuChannelName, cuChannelInfo.Name); + + // 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: cuChannelInfo.Rank)) + SendTo(user, new ChannelUpdateS2CPacket( + cuChannelName, + cuChannelInfo.Name, + cuChannelInfo.HasPassword, + cuChannelInfo.IsTemporary + )); + + if(nameChanged) + SendTo(cuChannelInfo, new UserChannelForceJoinS2CPacket(cuChannelInfo.Name)); + break; + + case "chan:delete": + ChannelInfo? cdTargetChannelInfo = Channels.Get(info.ChannelName); + ChannelInfo? cdMainChannelInfo = Channels.MainChannel; + if(cdTargetChannelInfo == null || cdMainChannelInfo == null || cdTargetChannelInfo == Channels.MainChannel) + break; + + // Remove channel from the listing + Channels.Remove(info.ChannelName); + + // Move all users back to the main channel + UserInfo[] cdUserInfos = Users.GetMany(ChannelsUsers.GetChannelUserIds(info.ChannelName)); + ChannelsUsers.DeleteChannel(cdMainChannelInfo); + + foreach(UserInfo userInfo in cdUserInfos) + HandleUserChannelJoin( + info.ChannelName, + userInfo, + UserStatuses.Get(userInfo) + ); + + // Broadcast deletion of channel + foreach(UserInfo user in Users.GetMany(minRank: cdTargetChannelInfo.Rank)) + SendTo(user, new ChannelDeleteS2CPacket(cdTargetChannelInfo.Name)); break; case "msg:delete": @@ -271,6 +378,37 @@ namespace SharpChat { } } + private void HandleUserChannelJoin(string channelName, UserInfo userInfo, UserStatusInfo statusInfo) { + SendTo(userInfo.UserId, new ClearMessagesAndUsersS2CPacket()); + + UserInfo[] userInfos = Users.GetMany(ChannelsUsers.GetChannelUserIds(channelName)); + List userEntries = new(); + foreach(UserInfo memberInfo in userInfos) + if(memberInfo.UserId != userInfo.UserId) + userEntries.Add(new( + memberInfo.UserId, + SockChatUtility.GetUserName(memberInfo, UserStatuses.Get(memberInfo)), + memberInfo.Colour, + memberInfo.Rank, + memberInfo.Permissions, + true + )); + SendTo(userInfo.UserId, new UsersPopulateS2CPacket(userEntries.ToArray())); + + SendTo(channelName, new UserChannelJoinS2CPacket( + userInfo.UserId, + SockChatUtility.GetUserName(userInfo, statusInfo), + userInfo.Colour, + userInfo.Rank, + userInfo.Permissions + )); + + HandleChannelEventLog(channelName, p => SendTo(userInfo.UserId, p)); + + ChannelsUsers.Join(channelName, userInfo.UserId); + SendTo(userInfo.UserId, new UserChannelForceJoinS2CPacket(channelName)); + } + public void Update() { ConnectionInfo[] timedOut = Connections.GetTimedOut(); foreach(ConnectionInfo conn in timedOut) { @@ -283,6 +421,7 @@ namespace SharpChat { foreach(UserInfo user in Users.All) if(!Connections.HasUser(user)) { + Events.Dispatch("user:delete", user); Events.Dispatch( "user:disconnect", ChannelsUsers.GetUserLastChannel(user), @@ -302,10 +441,6 @@ namespace SharpChat { } } - public UserInfo[] GetChannelUsers(ChannelInfo channel) { - return Users.GetMany(ChannelsUsers.GetChannelUserIds(channel)); - } - public void HandleChannelEventLog(string channelName, Action handler) { foreach(ChatEventInfo info in EventStorage.GetChannelEventLog(channelName)) { switch(info.Type) { @@ -330,11 +465,12 @@ namespace SharpChat { break; case "user:connect": - handler(new UserConnectLogS2CPacket( - info.Id, - info.Created, - SockChatUtility.GetUserName(info) - )); + if(info.Data is UserConnectEventData ucData && ucData.Notify) + handler(new UserConnectLogS2CPacket( + info.Id, + info.Created, + SockChatUtility.GetUserName(info) + )); break; case "user:disconnect": @@ -375,99 +511,6 @@ namespace SharpChat { } } - public void HandleJoin(UserInfo user, ChannelInfo chan, SockChatConnectionInfo conn, int maxMsgLength) { - if(!ChannelsUsers.Has(chan, user)) - Events.Dispatch("user:connect", chan, user); - - UserStatusInfo statusInfo = UserStatuses.Get(user); - - conn.Send(new AuthSuccessS2CPacket( - user.UserId, - SockChatUtility.GetUserName(user, statusInfo), - user.Colour, - user.Rank, - user.Permissions, - chan.Name, - maxMsgLength - )); - conn.Send(new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select( - user => new UsersPopulateS2CPacket.ListEntry( - user.UserId, - SockChatUtility.GetUserName(user, statusInfo), - user.Colour, - user.Rank, - user.Permissions, - true - ) - ).OrderByDescending(user => user.Rank).ToArray())); - - HandleChannelEventLog(chan.Name, p => conn.Send(p)); - - conn.Send(new ChannelsPopulateS2CPacket(Channels.GetMany(isPublic: true, minRank: user.Rank).Select( - channel => new ChannelsPopulateS2CPacket.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 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 ChannelRankTooLowErrorS2CPacket(chan.Name)); - ForceChannel(user); - return; - } - - if(!string.IsNullOrEmpty(chan.Password) && chan.Password.Equals(password)) { - SendTo(user, new ChannelPasswordWrongErrorS2CPacket(chan.Name)); - ForceChannel(user); - return; - } - } - - ForceChannelSwitch(user, chan); - } - - public void ForceChannelSwitch(UserInfo user, ChannelInfo chan) { - DateTimeOffset now = DateTimeOffset.UtcNow; - ChannelInfo? oldChan = Channels.Get(ChannelsUsers.GetUserLastChannel(user)); - - if(oldChan != null) - Events.Dispatch("chan:leave", now, oldChan, user); - - Events.Dispatch("chan:join", now, chan, user); - - SendTo(user, new ClearMessagesAndUsersS2CPacket()); - SendTo(user, new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select( - user => new UsersPopulateS2CPacket.ListEntry( - user.UserId, - SockChatUtility.GetUserName(user, UserStatuses.Get(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(ISockChatS2CPacket packet) { string data = packet.Pack(); Connections.WithAuthed(conn => { @@ -512,65 +555,11 @@ namespace SharpChat { }); } - public void SendToUserChannels(UserInfo user, ISockChatS2CPacket packet) { - SendToUserChannels(user.UserId, packet); - } - public void SendToUserChannels(long userId, ISockChatS2CPacket packet) { ChannelInfo[] chans = Channels.GetMany(ChannelsUsers.GetUserChannelNames(userId)); string data = packet.Pack(); foreach(ChannelInfo chan in chans) SendTo(chan, data); } - - public void ForceChannel(UserInfo user, ChannelInfo? chan = null) { - chan ??= Channels.Get(ChannelsUsers.GetUserLastChannel(user)); - if(chan != null) - SendTo(user, new UserChannelForceJoinS2CPacket(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 ChannelUpdateS2CPacket(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 ChannelDeleteS2CPacket(channel.Name)); - } } } diff --git a/SharpChat.SockChat/SockChatServer.cs b/SharpChat.SockChat/SockChatServer.cs index 94d7d39..c636027 100644 --- a/SharpChat.SockChat/SockChatServer.cs +++ b/SharpChat.SockChat/SockChatServer.cs @@ -71,10 +71,12 @@ namespace SharpChat.SockChat { if(Context.Channels.PublicCount < 1) Context.Channels.Add(new ChannelInfo("Default")); + CachedValue motdHeaderFormat = config.ReadCached("motd", @"Welcome to Flashii Chat, {0}!"); GuestHandlers.Add(new AuthC2SPacketHandler( started, Misuzu, Context.Channels.MainChannel, + motdHeaderFormat, MaxMessageLength, MaxConnections )); @@ -156,13 +158,15 @@ namespace SharpChat.SockChat { if(!Context.Connections.HasUser(conn.UserId)) { UserInfo? userInfo = Context.Users.Get(conn.UserId); - if(userInfo != null) + if(userInfo != null) { + Context.Events.Dispatch("user:delete", userInfo); Context.Events.Dispatch( "user:disconnect", - Context.ChannelsUsers.GetUserLastChannel(userInfo), + Context.ChannelsUsers.GetUserLastChannel(conn.UserId), userInfo, new UserDisconnectEventData(UserDisconnectReason.Leave) ); + } } Context.Update(); @@ -205,7 +209,7 @@ namespace SharpChat.SockChat { if(banDuration == TimeSpan.MinValue) { Context.SendTo(userInfo, new FloodWarningS2CPacket()); } else { - Context.Events.Dispatch("user:kickban", userInfo, new UserKickBanEventData(UserDisconnectReason.Flood, banDuration)); + Context.Events.Dispatch("user:kickban", userInfo, UserKickBanEventData.OfDuration(UserDisconnectReason.Flood, banDuration)); if(banDuration > TimeSpan.Zero) Misuzu.CreateBanAsync( diff --git a/SharpChatCommon/ChannelInfo.cs b/SharpChatCommon/ChannelInfo.cs index fafda1e..13d5025 100644 --- a/SharpChatCommon/ChannelInfo.cs +++ b/SharpChatCommon/ChannelInfo.cs @@ -1,6 +1,6 @@ namespace SharpChat { public class ChannelInfo { - public string Name { get; } + public string Name { get; set; } public string Password { get; set; } public bool IsTemporary { get; set; } public int Rank { get; set; } @@ -25,15 +25,5 @@ Rank = rank; OwnerId = ownerId; } - - public bool IsOwner(long userId) { - return OwnerId > 0 - && OwnerId == userId; - } - public bool IsOwner(UserInfo user) { - return OwnerId > 0 - && user != null - && OwnerId == user.UserId; - } } } diff --git a/SharpChatCommon/ChannelsContext.cs b/SharpChatCommon/ChannelsContext.cs index b5497a1..f9f85c5 100644 --- a/SharpChatCommon/ChannelsContext.cs +++ b/SharpChatCommon/ChannelsContext.cs @@ -4,13 +4,13 @@ using System.Linq; namespace SharpChat { public class ChannelsContext { - private readonly List Channels = new(); + private readonly Dictionary Channels = new(); public ChannelInfo? MainChannel { get; private set; } public int TotalCount { get; private set; } public int PublicCount { get; private set; } - public ChannelInfo[] All => Channels.ToArray(); + public ChannelInfo[] All => Channels.Values.ToArray(); public ChannelInfo? Get( string? name, @@ -19,7 +19,7 @@ namespace SharpChat { if(string.IsNullOrWhiteSpace(name)) return null; - foreach(ChannelInfo info in Channels) { + foreach(ChannelInfo info in Channels.Values) { string chanName = info.Name; if(sanitise != null) chanName = sanitise(chanName); @@ -44,7 +44,7 @@ namespace SharpChat { for(int i = 0; i < names.Length; ++i) names[i] = names[i].ToLowerInvariant(); - foreach(ChannelInfo info in Channels) { + foreach(ChannelInfo info in Channels.Values) { if(info.Rank > minRank) continue; @@ -71,18 +71,14 @@ namespace SharpChat { return chans.ToArray(); } - public void Add( - ChannelInfo info, - bool forceMain = false, - Func? sanitiseName = null - ) { - if(Get(info.Name, sanitiseName) != null) + public void Add(ChannelInfo info, bool forceMain = false) { + if(Channels.ContainsKey(info.Name.ToLowerInvariant())) throw new ArgumentException("A channel with this name has already been registered.", nameof(info)); if(string.IsNullOrWhiteSpace(info.Name)) throw new ArgumentException("Channel names may not be blank.", nameof(info)); // todo: there should be more restrictions on channel names - Channels.Add(info); + Channels.Add(info.Name.ToLowerInvariant(), info); ++TotalCount; if(info.IsPublic) @@ -110,14 +106,14 @@ namespace SharpChat { if(info == null) return; - Channels.Remove(info); + Channels.Remove(info.Name.ToLowerInvariant()); --TotalCount; if(info.IsPublic) --PublicCount; if(MainChannel == info) - MainChannel = Channels.FirstOrDefault(c => !c.IsPublic); + MainChannel = Channels.Values.FirstOrDefault(c => !c.IsPublic); } } } diff --git a/SharpChatCommon/ChannelsUsersContext.cs b/SharpChatCommon/ChannelsUsersContext.cs index 7fcbcdf..efefab8 100644 --- a/SharpChatCommon/ChannelsUsersContext.cs +++ b/SharpChatCommon/ChannelsUsersContext.cs @@ -253,5 +253,31 @@ namespace SharpChat { public void DeleteChannel(ChannelInfo channelInfo) { DeleteChannel(channelInfo.Name); } + + public void RenameChannel(ChannelInfo channelInfo, string newName) { + RenameChannel(channelInfo.Name, newName); + } + + public void RenameChannel(string prevName, string newName) { + newName = newName.ToLowerInvariant(); + if(ChannelUsers.ContainsKey(newName)) + throw new ArgumentException("A channel with that name is already registered!", nameof(newName)); + + prevName = prevName.ToLowerInvariant(); + if(ChannelUsers.ContainsKey(prevName)) { + ChannelUsers.Add(newName, ChannelUsers[prevName]); + ChannelUsers.Remove(prevName); + } + + foreach(KeyValuePair kvp in UserLastChannel) + if(kvp.Value.Equals(prevName, StringComparison.InvariantCultureIgnoreCase)) + UserLastChannel[kvp.Key] = newName; + + foreach(HashSet userChans in UserChannels.Values) + if(userChans.Contains(prevName)) { + userChans.Add(newName); + userChans.Remove(prevName); + } + } } } diff --git a/SharpChatCommon/EventStorage/MariaDBEventStorage_Database.cs b/SharpChatCommon/EventStorage/MariaDBEventStorage_Database.cs index b0789bc..3dab3d5 100644 --- a/SharpChatCommon/EventStorage/MariaDBEventStorage_Database.cs +++ b/SharpChatCommon/EventStorage/MariaDBEventStorage_Database.cs @@ -48,6 +48,22 @@ namespace SharpChat.EventStorage { return 0; } + private int RunMigrationCommand(string command, params MySqlParameter[] parameters) { + try { + using MySqlConnection conn = GetConnection(); + using MySqlCommand cmd = conn.CreateCommand(); + cmd.CommandTimeout = 0; + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return cmd.ExecuteNonQuery(); + } catch(MySqlException ex) { + Logger.Write(ex); + } + + return 0; + } + private MySqlDataReader? RunQuery(string command, params MySqlParameter[] parameters) { try { MySqlConnection conn = GetConnection(); diff --git a/SharpChatCommon/EventStorage/MariaDBEventStorage_Migrations.cs b/SharpChatCommon/EventStorage/MariaDBEventStorage_Migrations.cs index 5aa2307..5da5baa 100644 --- a/SharpChatCommon/EventStorage/MariaDBEventStorage_Migrations.cs +++ b/SharpChatCommon/EventStorage/MariaDBEventStorage_Migrations.cs @@ -11,7 +11,7 @@ namespace SharpChat.EventStorage { if(!done) { Logger.Write($"Running migration '{name}'..."); action(); - RunCommand( + RunMigrationCommand( "INSERT INTO `sqc_migrations` (`migration_name`) VALUES (@name)", new MySqlParameter("name", name) ); @@ -19,7 +19,7 @@ namespace SharpChat.EventStorage { } public void RunMigrations() { - RunCommand( + RunMigrationCommand( "CREATE TABLE IF NOT EXISTS `sqc_migrations` (" + "`migration_name` VARCHAR(255) NOT NULL," + "`migration_completed` TIMESTAMP NOT NULL DEFAULT current_timestamp()," @@ -36,9 +36,9 @@ namespace SharpChat.EventStorage { } private void UpdateCollationsAndUseJsonType() { - RunCommand("UPDATE sqc_events SET event_target = LOWER(event_target)"); - RunCommand("UPDATE sqc_events SET event_sender_nick = NULL WHERE event_sender_nick = ''"); - RunCommand( + RunMigrationCommand("UPDATE sqc_events SET event_target = LOWER(event_target)"); + RunMigrationCommand("UPDATE sqc_events SET event_sender_nick = NULL WHERE event_sender_nick = ''"); + RunMigrationCommand( "ALTER TABLE `sqc_events` COLLATE='utf8mb4_unicode_520_ci'," + " CHANGE COLUMN `event_type` `event_type` VARCHAR(255) NOT NULL COLLATE 'ascii_general_ci' AFTER `event_id`," + " CHANGE COLUMN `event_created` `event_created` TIMESTAMP NOT NULL DEFAULT current_timestamp() AFTER `event_type`," @@ -53,33 +53,33 @@ namespace SharpChat.EventStorage { private void DeprecateEventFlags() { // StoredEventFlags.Action is just a field in the data object - RunCommand(@"UPDATE sqc_events SET event_data = JSON_MERGE_PATCH(event_data, JSON_OBJECT('action', true)) WHERE event_flags & 1"); + RunMigrationCommand(@"UPDATE sqc_events SET event_data = JSON_MERGE_PATCH(event_data, JSON_OBJECT('action', true)) WHERE event_flags & 1"); // StoredEventFlags.Broadcast can be implied by just having a NULL as the channel name - RunCommand(@"UPDATE sqc_events SET event_target = NULL WHERE event_flags & 2"); + RunMigrationCommand(@"UPDATE sqc_events SET event_target = NULL WHERE event_flags & 2"); // StoredEventFlags.Log was never meaningfully used by anything and basically just meant "not-msg:add" // StoredEventFlags.Private was also never meaningfully used, can be determined by checking if the channel name starts with @ - RunCommand(@"ALTER TABLE sqc_events DROP COLUMN event_flags"); + RunMigrationCommand(@"ALTER TABLE sqc_events DROP COLUMN event_flags"); } private void UpdateEventTypeNames() { - RunCommand(@"UPDATE sqc_events SET event_type = ""msg:add"" WHERE event_type = ""SharpChat.Events.ChatMessage"""); - RunCommand(@"UPDATE sqc_events SET event_type = ""user:connect"" WHERE event_type = ""SharpChat.Events.UserConnectEvent"""); - RunCommand(@"UPDATE sqc_events SET event_type = ""user:disconnect"" WHERE event_type = ""SharpChat.Events.UserDisconnectEvent"""); - RunCommand(@"UPDATE sqc_events SET event_type = ""chan:join"" WHERE event_type = ""SharpChat.Events.UserChannelJoinEvent"""); - RunCommand(@"UPDATE sqc_events SET event_type = ""chan:leave"" WHERE event_type = ""SharpChat.Events.UserChannelLeaveEvent"""); + RunMigrationCommand(@"UPDATE sqc_events SET event_type = ""msg:add"" WHERE event_type = ""SharpChat.Events.ChatMessage"""); + RunMigrationCommand(@"UPDATE sqc_events SET event_type = ""user:connect"" WHERE event_type = ""SharpChat.Events.UserConnectEvent"""); + RunMigrationCommand(@"UPDATE sqc_events SET event_type = ""user:disconnect"" WHERE event_type = ""SharpChat.Events.UserDisconnectEvent"""); + RunMigrationCommand(@"UPDATE sqc_events SET event_type = ""chan:join"" WHERE event_type = ""SharpChat.Events.UserChannelJoinEvent"""); + RunMigrationCommand(@"UPDATE sqc_events SET event_type = ""chan:leave"" WHERE event_type = ""SharpChat.Events.UserChannelLeaveEvent"""); } private void AllowNullTarget() { - RunCommand( + RunMigrationCommand( "ALTER TABLE `sqc_events`" + " CHANGE COLUMN `event_target` `event_target` VARBINARY(255) NULL AFTER `event_type`;" ); } private void CreateEventsTable() { - RunCommand( + RunMigrationCommand( "CREATE TABLE `sqc_events` (" + "`event_id` BIGINT(20) NOT NULL," + "`event_sender` BIGINT(20) UNSIGNED NULL DEFAULT NULL," diff --git a/SharpChatCommon/Events/ChannelAddEventData.cs b/SharpChatCommon/Events/ChannelAddEventData.cs new file mode 100644 index 0000000..8dca4fd --- /dev/null +++ b/SharpChatCommon/Events/ChannelAddEventData.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + [ChatEventDataFor("chan:add")] + public class ChannelAddEventData : ChatEventData { + [JsonPropertyName("temp")] + public bool IsTemporary { get; } + + [JsonPropertyName("rank")] + public int MinRank { get; } + + [JsonPropertyName("pass")] + public string Password { get; } + + public ChannelAddEventData(bool isTemporary, int minRank, string password) { + IsTemporary = isTemporary; + MinRank = minRank; + Password = password; + } + } +} diff --git a/SharpChatCommon/Events/ChannelUpdateEventData.cs b/SharpChatCommon/Events/ChannelUpdateEventData.cs new file mode 100644 index 0000000..cf6bbe7 --- /dev/null +++ b/SharpChatCommon/Events/ChannelUpdateEventData.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + [ChatEventDataFor("chan:update")] + public class ChannelUpdateEventData : ChatEventData { + [JsonPropertyName("name")] + public string? Name { get; } + + [JsonPropertyName("temp")] + public bool? IsTemporary { get; } + + [JsonPropertyName("rank")] + public int? MinRank { get; } + + [JsonPropertyName("pass")] + public string? Password { get; } + + [JsonPropertyName("owner")] + public long? OwnerId { get; set; } + + public ChannelUpdateEventData( + string? name = null, + bool? isTemporary = null, + int? minRank = null, + string? password = null, + long? ownerId = null + ) { + Name = name; + IsTemporary = isTemporary; + MinRank = minRank; + Password = password; + OwnerId = ownerId; + } + } +} diff --git a/SharpChatCommon/Events/ChatEventDispatcher.cs b/SharpChatCommon/Events/ChatEventDispatcher.cs index 0d6a909..2723735 100644 --- a/SharpChatCommon/Events/ChatEventDispatcher.cs +++ b/SharpChatCommon/Events/ChatEventDispatcher.cs @@ -25,6 +25,53 @@ namespace SharpChat.Events { return info; } + public ChatEventInfo Dispatch( + string eventType, + long senderId, + string senderName, + Colour senderColour, + int senderRank, + string senderNickName, + UserPermissions senderPerms, + ChatEventData? eventData = null + ) { + return Dispatch(new ChatEventInfo( + SharpId.Next(), + eventType, + DateTimeOffset.UtcNow, + string.Empty, + senderId, + senderName, + senderColour, + senderRank, + senderNickName, + senderPerms, + eventData + )); + } + + public ChatEventInfo Dispatch( + string eventType, + DateTimeOffset eventCreated, + string channelName, + UserInfo userInfo, + ChatEventData? eventData = null + ) { + return Dispatch(new ChatEventInfo( + SharpId.Next(), + eventType, + eventCreated, + channelName, + userInfo.UserId, + userInfo.UserName, + userInfo.Colour, + userInfo.Rank, + userInfo.NickName, + userInfo.Permissions, + eventData + )); + } + public ChatEventInfo Dispatch( string eventType, DateTimeOffset eventCreated, @@ -47,6 +94,27 @@ namespace SharpChat.Events { )); } + public ChatEventInfo Dispatch( + string eventType, + string channelName, + ChatEventInfo userInfo, + ChatEventData? eventData = null + ) { + return Dispatch(new ChatEventInfo( + SharpId.Next(), + eventType, + DateTimeOffset.UtcNow, + channelName, + userInfo.SenderId, + userInfo.SenderName, + userInfo.SenderColour, + userInfo.SenderRank, + userInfo.SenderNickName, + userInfo.SenderPerms, + eventData + )); + } + public ChatEventInfo Dispatch( string eventType, string channelName, @@ -84,5 +152,13 @@ namespace SharpChat.Events { ) { return Dispatch(eventType, string.Empty, userInfo, eventData); } + + public ChatEventInfo Dispatch( + string eventType, + ChatEventInfo userInfo, + ChatEventData? eventData = null + ) { + return Dispatch(eventType, string.Empty, userInfo, eventData); + } } } diff --git a/SharpChatCommon/Events/UserAddEventData.cs b/SharpChatCommon/Events/UserAddEventData.cs new file mode 100644 index 0000000..5edf422 --- /dev/null +++ b/SharpChatCommon/Events/UserAddEventData.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + [ChatEventDataFor("user:add")] + public class UserAddEventData : ChatEventData { + [JsonPropertyName("super")] + public bool IsSuper { get; } + + public UserAddEventData(bool isSuper) { + IsSuper = isSuper; + } + } +} diff --git a/SharpChatCommon/Events/UserConnectEventData.cs b/SharpChatCommon/Events/UserConnectEventData.cs new file mode 100644 index 0000000..95bbdaf --- /dev/null +++ b/SharpChatCommon/Events/UserConnectEventData.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + [ChatEventDataFor("user:connect")] + public class UserConnectEventData : ChatEventData { + [JsonPropertyName("notify")] + public bool Notify { get; } + + public UserConnectEventData(bool notify) { + Notify = notify; + } + } +} diff --git a/SharpChatCommon/Events/UserKickBanEventData.cs b/SharpChatCommon/Events/UserKickBanEventData.cs index 17ea4b9..8484497 100644 --- a/SharpChatCommon/Events/UserKickBanEventData.cs +++ b/SharpChatCommon/Events/UserKickBanEventData.cs @@ -18,9 +18,11 @@ namespace SharpChat.Events { Expires = expires; } - public UserKickBanEventData( + public static UserKickBanEventData OfDuration( UserDisconnectReason reason, TimeSpan duration - ) : this(reason, DateTimeOffset.UtcNow.Add(duration)) {} + ) { + return new UserKickBanEventData(reason, DateTimeOffset.UtcNow.Add(duration)); + } } } diff --git a/SharpChatCommon/UserInfo.cs b/SharpChatCommon/UserInfo.cs index 3e03a7e..6c18c85 100644 --- a/SharpChatCommon/UserInfo.cs +++ b/SharpChatCommon/UserInfo.cs @@ -1,4 +1,6 @@ -namespace SharpChat { +using SharpChat.Events; + +namespace SharpChat { public class UserInfo { public const int DEFAULT_SIZE = 30; public const int DEFAULT_MINIMUM_DELAY = 10000; @@ -30,6 +32,16 @@ IsSuper = isSuper; } + public UserInfo(ChatEventInfo eventInfo) + : this( + eventInfo.SenderId, + eventInfo.SenderName, + eventInfo.SenderColour, + eventInfo.SenderRank, + eventInfo.SenderPerms, + eventInfo.SenderNickName + ) {} + public static string GetDMChannelName(UserInfo user1, UserInfo user2) { return user1.UserId < user2.UserId ? $"@{user1.UserId}-{user2.UserId}" diff --git a/SharpChatCommon/UsersContext.cs b/SharpChatCommon/UsersContext.cs index 17e6179..5c6deaa 100644 --- a/SharpChatCommon/UsersContext.cs +++ b/SharpChatCommon/UsersContext.cs @@ -12,18 +12,18 @@ namespace SharpChat { UserAndNickName = UserName | NickName, } - private readonly List Users = new(); + private readonly Dictionary Users = new(); public int TotalCount { get; private set; } - public UserInfo[] All => Users.ToArray(); + public UserInfo[] All => Users.Values.ToArray(); public UserInfo? Get( long? userId = null, string? name = null, NameTarget nameTarget = NameTarget.UserName ) { - foreach(UserInfo info in Users) { + foreach(UserInfo info in Users.Values) { if(userId != null && info.UserId != userId) continue; if(name != null) { @@ -52,7 +52,7 @@ namespace SharpChat { ) { List users = new(); - foreach(UserInfo info in Users) { + foreach(UserInfo info in Users.Values) { if(minRank != null && info.Rank < minRank) continue; @@ -79,34 +79,32 @@ namespace SharpChat { } public void Add(UserInfo info) { - if(Get(info.UserId, info.UserName) != null) - throw new ArgumentException("A user with that id and/or name has already been registred.", nameof(info)); + if(Users.ContainsKey(info.UserId)) + return; - Users.Add(info); + Users.Add(info.UserId, info); ++TotalCount; } public void Remove(UserInfo info) { - if(!Users.Contains(info)) { - Remove(info.UserId); - return; - } + Remove(info.UserId); + } - Users.Remove(info); + public void Remove(long userId) { + if(!Users.ContainsKey(userId)) + return; + + Users.Remove(userId); --TotalCount; } public void Remove( - long? userId = null, - string? name = null, + string name, NameTarget nameTarget = NameTarget.UserName ) { - UserInfo? info = Get(userId, name, nameTarget); - if(info == null) - return; - - Users.Remove(info); - --TotalCount; + UserInfo? info = Get(name: name, nameTarget: nameTarget); + if(info != null) + Remove(info.UserId); } } }