Changed pretty much every context mutation into an event.

Don't run this in prod yet lol.
This commit is contained in:
flash 2024-05-29 20:51:37 +00:00
parent 6bda1ee09d
commit 42a0160cde
23 changed files with 584 additions and 305 deletions

View file

@ -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));
}
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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))
));
}

View file

@ -15,6 +15,7 @@ namespace SharpChat.SockChat.PacketsC2S {
private readonly DateTimeOffset Started;
private readonly MisuzuClient Misuzu;
private readonly ChannelInfo DefaultChannel;
private readonly CachedValue<string> MOTDHeaderFormat;
private readonly CachedValue<int> MaxMessageLength;
private readonly CachedValue<int> MaxConnections;
@ -22,12 +23,14 @@ namespace SharpChat.SockChat.PacketsC2S {
DateTimeOffset started,
MisuzuClient msz,
ChannelInfo? defaultChannel,
CachedValue<string> motdHeaderFormat,
CachedValue<int> maxMsgLength,
CachedValue<int> 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<string> 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<UsersPopulateS2CPacket.ListEntry> 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<ChannelsPopulateS2CPacket.ListEntry> 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();
}

View file

@ -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<UsersPopulateS2CPacket.ListEntry> 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<ISockChatS2CPacket> 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));
}
}
}

View file

@ -71,10 +71,12 @@ namespace SharpChat.SockChat {
if(Context.Channels.PublicCount < 1)
Context.Channels.Add(new ChannelInfo("Default"));
CachedValue<string> 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(

View file

@ -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;
}
}
}

View file

@ -4,13 +4,13 @@ using System.Linq;
namespace SharpChat {
public class ChannelsContext {
private readonly List<ChannelInfo> Channels = new();
private readonly Dictionary<string, ChannelInfo> 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<string, string>? 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);
}
}
}

View file

@ -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<long, string> kvp in UserLastChannel)
if(kvp.Value.Equals(prevName, StringComparison.InvariantCultureIgnoreCase))
UserLastChannel[kvp.Key] = newName;
foreach(HashSet<string> userChans in UserChannels.Values)
if(userChans.Contains(prevName)) {
userChans.Add(newName);
userChans.Remove(prevName);
}
}
}
}

View file

@ -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();

View file

@ -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,"

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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));
}
}
}

View file

@ -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}"

View file

@ -12,18 +12,18 @@ namespace SharpChat {
UserAndNickName = UserName | NickName,
}
private readonly List<UserInfo> Users = new();
private readonly Dictionary<long, UserInfo> 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<UserInfo> 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);
}
}
}