Rewrote user and channel collections.

This commit is contained in:
flash 2024-05-19 21:02:17 +00:00
parent 1a8c44a4ba
commit 549c80740d
50 changed files with 782 additions and 243 deletions

View file

@ -1,7 +1,4 @@
using System;
using System.Linq;
namespace SharpChat {
namespace SharpChat {
public class ChannelInfo {
public string Name { get; }
public string Password { get; set; }
@ -12,6 +9,9 @@ namespace SharpChat {
public bool HasPassword
=> !string.IsNullOrWhiteSpace(Password);
public bool IsPublic
=> !IsTemporary && Rank < 1 && !HasPassword;
public ChannelInfo(
string name,
string? password = null,
@ -26,22 +26,10 @@ namespace SharpChat {
OwnerId = ownerId;
}
public bool NameEquals(string? name) {
return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase);
}
public bool IsOwner(UserInfo user) {
return OwnerId > 0
&& user != null
&& OwnerId == user.UserId;
}
public static bool CheckName(string name) {
return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar);
}
public static bool CheckNameChar(char c) {
return char.IsLetter(c) || char.IsNumber(c) || c == '-' || c == '_';
}
}
}

View file

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat {
public class ChannelsContext {
private readonly List<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? Get(
string? name,
Func<string, string>? sanitise = null
) {
if(string.IsNullOrWhiteSpace(name))
return null;
foreach(ChannelInfo info in Channels) {
string chanName = info.Name;
if(sanitise != null)
chanName = sanitise(chanName);
if(!chanName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
continue;
return info;
}
return null;
}
public ChannelInfo[] GetMany(
string[]? names = null,
Func<string, string>? sanitiseName = null,
int minRank = 0,
bool? isPublic = null
) {
List<ChannelInfo> chans = new();
names ??= Array.Empty<string>();
for(int i = 0; i < names.Length; ++i)
names[i] = names[i].ToLowerInvariant();
foreach(ChannelInfo info in Channels) {
if(info.Rank > minRank)
continue;
if(isPublic != null && info.IsPublic != isPublic)
continue;
if(names?.Length > 0) {
string chanName = info.Name;
if(sanitiseName != null)
chanName = sanitiseName(chanName);
bool match = false;
foreach(string name in names)
if(match = chanName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
break;
if(!match)
continue;
}
chans.Add(info);
}
return chans.ToArray();
}
public void Add(
ChannelInfo info,
bool forceMain = false,
Func<string, string>? sanitiseName = null
) {
if(Get(info.Name, sanitiseName) != null)
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);
++TotalCount;
if(info.IsPublic)
++PublicCount;
if(forceMain || MainChannel == null)
MainChannel = info;
}
public void Remove(
ChannelInfo info,
Func<string, string>? sanitiseName = null
) {
Remove(info.Name, sanitiseName);
}
public void Remove(
string? name,
Func<string, string>? sanitise = null
) {
if(string.IsNullOrWhiteSpace(name))
return;
ChannelInfo? info = Get(name, sanitise);
if(info == null)
return;
Channels.Remove(info);
--TotalCount;
if(info.IsPublic)
--PublicCount;
if(MainChannel == info)
MainChannel = Channels.FirstOrDefault(c => !c.IsPublic);
}
}
}

View file

@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat {
public class ChannelsUsersContext {
private readonly Dictionary<string, HashSet<long>> ChannelUsers = new();
private readonly Dictionary<long, HashSet<string>> UserChannels = new();
private readonly Dictionary<long, string> UserLastChannel = new();
public string GetUserLastChannel(long userId) {
return UserLastChannel.ContainsKey(userId)
? UserLastChannel[userId]
: string.Empty;
}
public string GetUserLastChannel(UserInfo userInfo) {
return GetUserLastChannel(userInfo.UserId);
}
public void SetUserLastChannel(long userId, string channelName) {
channelName = channelName.ToLowerInvariant();
if(UserLastChannel.ContainsKey(userId))
UserLastChannel[userId] = channelName;
else
UserLastChannel.Add(userId, channelName);
}
public void SetUserLastChannel(UserInfo userInfo, ChannelInfo channelInfo) {
SetUserLastChannel(userInfo.UserId, channelInfo.Name);
}
public void DeleteUserLastChannel(long userId) {
if(UserLastChannel.ContainsKey(userId))
UserLastChannel.Remove(userId);
}
public void DeleteUserLastChannel(UserInfo userInfo) {
DeleteUserLastChannel(userInfo.UserId);
}
public bool IsUserLastChannel(long userId, string channelName) {
return !string.IsNullOrWhiteSpace(channelName)
&& GetUserLastChannel(userId).Equals(channelName, StringComparison.InvariantCultureIgnoreCase);
}
public bool IsUserLastChannel(UserInfo userInfo, ChannelInfo channelInfo) {
return IsUserLastChannel(userInfo.UserId, channelInfo.Name);
}
public string[] GetUserChannelNames(long userId) {
if(!UserChannels.ContainsKey(userId))
return Array.Empty<string>();
return UserChannels[userId].ToArray();
}
public string[] GetUserChannelNames(UserInfo userInfo) {
return GetUserChannelNames(userInfo.UserId);
}
public long[] GetChannelUserIds(string channelName) {
channelName = channelName.ToLowerInvariant();
if(!ChannelUsers.ContainsKey(channelName))
return Array.Empty<long>();
return ChannelUsers[channelName].ToArray();
}
public long[] GetChannelUserIds(string channelName, Func<string, string> sanitise) {
foreach(KeyValuePair<string, HashSet<long>> kvp in ChannelUsers)
if(sanitise(kvp.Key).Equals(channelName, StringComparison.InvariantCultureIgnoreCase))
return kvp.Value.ToArray();
return Array.Empty<long>();
}
public long[] GetChannelUserIds(ChannelInfo channelInfo) {
return GetChannelUserIds(channelInfo.Name);
}
public void Join(string channelName, long userId) {
channelName = channelName.ToLowerInvariant();
if(ChannelUsers.ContainsKey(channelName))
ChannelUsers[channelName].Add(userId);
else
ChannelUsers.Add(channelName, new HashSet<long> { userId });
if(UserChannels.ContainsKey(userId))
UserChannels[userId].Add(channelName);
else
UserChannels.Add(userId, new HashSet<string> { channelName });
SetUserLastChannel(userId, channelName);
}
public void Join(ChannelInfo channelInfo, UserInfo userInfo) {
Join(channelInfo.Name, userInfo.UserId);
}
public void Leave(string channelName, long userId) {
channelName = channelName.ToLowerInvariant();
if(ChannelUsers.ContainsKey(channelName)) {
if(ChannelUsers[channelName].Count < 2)
ChannelUsers.Remove(channelName);
else
ChannelUsers[channelName].Remove(userId);
}
if(UserChannels.ContainsKey(userId)) {
if(UserChannels[userId].Count < 2)
UserChannels.Remove(userId);
else
UserChannels[userId].Remove(channelName);
}
if(IsUserLastChannel(userId, channelName))
DeleteUserLastChannel(userId);
}
public void Leave(ChannelInfo channelInfo, UserInfo userInfo) {
Leave(channelInfo.Name, userInfo.UserId);
}
public bool Has(string channelName, long userId) {
channelName = channelName.ToLowerInvariant();
return ChannelUsers.ContainsKey(channelName)
&& ChannelUsers[channelName].Contains(userId);
}
public bool Has(ChannelInfo channelInfo, UserInfo userInfo) {
return Has(channelInfo.Name, userInfo.UserId);
}
public long[] FilterUsers(string channelName, long[] userIds) {
if(userIds.Length < 1)
return userIds;
channelName = channelName.ToLowerInvariant();
if(!ChannelUsers.ContainsKey(channelName))
return Array.Empty<long>();
List<long> filtered = new();
HashSet<long> channelUserIds = ChannelUsers[channelName];
foreach(long userId in userIds)
if(channelUserIds.Contains(userId))
filtered.Add(userId);
return filtered.ToArray();
}
public UserInfo[] FilterUsers(ChannelInfo channelInfo, UserInfo[] userInfos) {
if(userInfos.Length < 1)
return userInfos;
long[] filteredIds = FilterUsers(channelInfo.Name, userInfos.Select(u => u.UserId).ToArray());
if(filteredIds.Length < 1)
return Array.Empty<UserInfo>();
return userInfos.Where(u => filteredIds.Contains(u.UserId)).ToArray();
}
public bool HasUsers(string channelName, long[] userIds) {
return FilterUsers(channelName, userIds).SequenceEqual(userIds);
}
public bool HasUsers(ChannelInfo channelInfo, UserInfo[] userInfos) {
return HasUsers(channelInfo.Name, userInfos.Select(u => u.UserId).ToArray());
}
public string[] FilterChannels(long userId, string[] channelNames) {
if(channelNames.Length < 1)
return channelNames;
if(!UserChannels.ContainsKey(userId))
return Array.Empty<string>();
List<string> filtered = new();
HashSet<string> userChannelNames = UserChannels[userId];
foreach(string channelName in userChannelNames)
if(userChannelNames.Contains(channelName))
filtered.Add(channelName);
return filtered.ToArray();
}
public ChannelInfo[] FilterChannels(UserInfo userInfo, ChannelInfo[] channelInfos) {
if(channelInfos.Length < 1)
return channelInfos;
string[] filteredNames = FilterChannels(userInfo.UserId, channelInfos.Select(c => c.Name).ToArray());
if(filteredNames.Length < 1)
return Array.Empty<ChannelInfo>();
return channelInfos.Where(c => filteredNames.Contains(c.Name.ToLowerInvariant())).ToArray();
}
public bool HasChannels(long userId, string[] channelNames) {
if(!UserChannels.ContainsKey(userId))
return false;
HashSet<string> userChannelNames = UserChannels[userId];
foreach(string channelName in channelNames)
if(!userChannelNames.Contains(channelName.ToLowerInvariant()))
return false;
return true;
}
public bool HasChannels(UserInfo userInfo, ChannelInfo[] channelInfos) {
return HasChannels(userInfo.UserId, channelInfos.Select(c => c.Name).ToArray());
}
public void DeleteUser(long userId) {
if(!UserChannels.ContainsKey(userId))
return;
HashSet<string> channelNames = UserChannels[userId];
UserChannels.Remove(userId);
DeleteUserLastChannel(userId);
foreach(string channelName in channelNames) {
if(!ChannelUsers.ContainsKey(channelName))
continue;
ChannelUsers[channelName].Remove(userId);
}
}
public void DeleteUser(UserInfo userInfo) {
DeleteUser(userInfo.UserId);
}
public void DeleteChannel(string channelName) {
channelName = channelName.ToLowerInvariant();
if(!ChannelUsers.ContainsKey(channelName))
return;
HashSet<long> userIds = ChannelUsers[channelName];
ChannelUsers.Remove(channelName);
foreach(long userId in userIds) {
if(!UserChannels.ContainsKey(userId))
continue;
UserChannels[userId].Remove(channelName);
if(IsUserLastChannel(userId, channelName))
DeleteUserLastChannel(userId);
}
}
public void DeleteChannel(ChannelInfo channelInfo) {
DeleteChannel(channelInfo.Name);
}
}
}

View file

@ -9,17 +9,14 @@ using System.Threading;
namespace SharpChat {
public class ChatContext {
public record ChannelUserAssoc(long UserId, string ChannelName);
public readonly SemaphoreSlim ContextAccess = new(1, 1);
public Dictionary<string, ChannelInfo> Channels { get; } = new();
public ChannelsContext Channels { get; } = new();
public List<ConnectionInfo> Connections { get; } = new();
public Dictionary<long, UserInfo> Users { get; } = new();
public UsersContext Users { get; } = new();
public IEventStorage Events { get; }
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = new();
public ChannelsUsersContext ChannelsUsers { get; } = new();
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
public Dictionary<long, ChannelInfo> UserLastChannel { get; } = new();
public ChatContext(IEventStorage evtStore) {
Events = evtStore;
@ -41,7 +38,7 @@ namespace SharpChat {
if(targetIds.Length != 2)
return;
UserInfo[] users = Users.Where(kvp => targetIds.Contains(kvp.Key)).Select(kvp => kvp.Value).ToArray();
UserInfo[] users = Users.GetMany(targetIds);
UserInfo? target = users.FirstOrDefault(u => u.UserId != mce.SenderId);
if(target == null)
return;
@ -51,12 +48,12 @@ namespace SharpChat {
mce.MessageId,
DateTimeOffset.Now,
mce.SenderId,
mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText,
mce.SenderId == user.UserId ? $"{SockChatUtility.GetUserName(target)} {mce.MessageText}" : mce.MessageText,
mce.IsAction,
true
));
} else {
ChannelInfo? channel = Channels.Values.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
ChannelInfo? channel = Channels.Get(mce.ChannelName, SockChatUtility.SanitiseChannelName);
if(channel != null)
SendTo(channel, new MessageAddPacket(
mce.MessageId,
@ -92,7 +89,7 @@ namespace SharpChat {
if(removed > 0)
Logger.Write($"Removed {removed} nuked connections from the list.");
foreach(UserInfo user in Users.Values)
foreach(UserInfo user in Users.All)
if(!Connections.Any(conn => conn.User == user)) {
HandleDisconnect(user, UserDisconnectReason.TimeOut);
Logger.Write($"Timed out {user} (no more connections).");
@ -108,28 +105,12 @@ namespace SharpChat {
}
}
public bool IsInChannel(UserInfo? user, ChannelInfo? channel) {
return user != null
&& channel != null
&& ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name));
}
public string[] GetUserChannelNames(UserInfo user) {
return ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName).ToArray();
}
public ChannelInfo[] GetUserChannels(UserInfo user) {
string[] names = GetUserChannelNames(user);
return Channels.Values.Where(c => names.Any(n => c.NameEquals(n))).ToArray();
}
public long[] GetChannelUserIds(ChannelInfo channel) {
return ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId).ToArray();
return Channels.GetMany(ChannelsUsers.GetUserChannelNames(user));
}
public UserInfo[] GetChannelUsers(ChannelInfo channel) {
long[] targetIds = GetChannelUserIds(channel);
return Users.Values.Where(u => targetIds.Contains(u.UserId)).ToArray();
return Users.GetMany(ChannelsUsers.GetChannelUserIds(channel));
}
public void UpdateUser(
@ -192,11 +173,11 @@ namespace SharpChat {
if(hasChanged) {
if(previousName != null)
SendToUserChannels(user, new UserUpdateNotificationPacket(previousName, user.LegacyNameWithStatus));
SendToUserChannels(user, new UserUpdateNotificationPacket(previousName, SockChatUtility.GetUserNameWithStatus(user)));
SendToUserChannels(user, new UserUpdatePacket(
user.UserId,
user.LegacyNameWithStatus,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions
@ -222,10 +203,10 @@ namespace SharpChat {
public void HandleChannelEventLog(string channelName, Action<IServerPacket> handler) {
foreach(StoredEventInfo msg in Events.GetChannelEventLog(channelName))
handler(msg.Type switch {
"user:connect" => new UserConnectLogPacket(msg.Created, msg.Sender?.LegacyName ?? string.Empty),
"user:connect" => new UserConnectLogPacket(msg.Created, msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender)),
"user:disconnect" => new UserDisconnectLogPacket(
msg.Created,
msg.Sender?.LegacyNameWithStatus ?? string.Empty,
msg.Sender == null ? string.Empty : SockChatUtility.GetUserNameWithStatus(msg.Sender),
(UserDisconnectReason)msg.Data.RootElement.GetProperty("reason").GetByte()
),
_ => new MessagePopulatePacket(msg),
@ -233,11 +214,11 @@ namespace SharpChat {
}
public void HandleJoin(UserInfo user, ChannelInfo chan, ConnectionInfo conn, int maxMsgLength) {
if(!IsInChannel(user, chan)) {
if(!ChannelsUsers.Has(chan, user)) {
SendTo(chan, new UserConnectPacket(
DateTimeOffset.Now,
user.UserId,
user.LegacyNameWithStatus,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions
@ -247,7 +228,7 @@ namespace SharpChat {
conn.Send(new AuthSuccessPacket(
user.UserId,
user.LegacyNameWithStatus,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions,
@ -255,32 +236,42 @@ namespace SharpChat {
maxMsgLength
));
conn.Send(new UsersPopulatePacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new UsersPopulatePacket.ListEntry(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions, true)
user => new UsersPopulatePacket.ListEntry(
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions,
true
)
).OrderByDescending(user => user.Rank).ToArray()));
HandleChannelEventLog(chan.Name, p => conn.Send(p));
conn.Send(new ChannelsPopulatePacket(Channels.Values.Where(c => c.Rank <= user.Rank).Select(
conn.Send(new ChannelsPopulatePacket(Channels.GetMany(isPublic: true, minRank: user.Rank).Select(
channel => new ChannelsPopulatePacket.ListEntry(channel.Name, channel.HasPassword, channel.IsTemporary)
).ToArray()));
Users.Add(user.UserId, user);
if(Users.Get(userId: user.UserId) == null)
Users.Add(user);
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
UserLastChannel[user.UserId] = chan;
ChannelsUsers.Join(chan.Name, user.UserId);
}
public void HandleDisconnect(UserInfo user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
UpdateUser(user, status: UserStatus.Offline);
Users.Remove(user.UserId);
UserLastChannel.Remove(user.UserId);
ChannelInfo[] channels = GetUserChannels(user);
ChannelsUsers.DeleteUser(user);
foreach(ChannelInfo chan in channels) {
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));
SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, reason));
SendTo(chan, new UserDisconnectPacket(
DateTimeOffset.Now,
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
reason
));
Events.AddEvent("user:disconnect", user, chan, new { reason = (int)reason }, StoredEventFlags.Log);
if(chan.IsTemporary && chan.IsOwner(user))
@ -289,7 +280,7 @@ namespace SharpChat {
}
public void SwitchChannel(UserInfo user, ChannelInfo chan, string password) {
if(UserLastChannel.TryGetValue(user.UserId, out ChannelInfo? ulc) && chan == ulc) {
if(ChannelsUsers.IsUserLastChannel(user, chan)) {
ForceChannel(user);
return;
}
@ -312,29 +303,44 @@ namespace SharpChat {
}
public void ForceChannelSwitch(UserInfo user, ChannelInfo chan) {
if(!Channels.ContainsValue(chan))
return;
ChannelInfo? oldChan = Channels.Get(ChannelsUsers.GetUserLastChannel(user));
ChannelInfo oldChan = UserLastChannel[user.UserId];
if(oldChan != null) {
SendTo(oldChan, new UserChannelLeavePacket(user.UserId));
Events.AddEvent("chan:leave", user, oldChan, flags: StoredEventFlags.Log);
}
SendTo(oldChan, new UserChannelLeavePacket(user.UserId));
Events.AddEvent("chan:leave", user, oldChan, flags: StoredEventFlags.Log);
SendTo(chan, new UserChannelJoinPacket(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
Events.AddEvent("chan:join", user, oldChan, flags: StoredEventFlags.Log);
SendTo(chan, new UserChannelJoinPacket(
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions
));
if(oldChan != null)
Events.AddEvent("chan:join", user, oldChan, flags: StoredEventFlags.Log);
SendTo(user, new ContextClearPacket(ContextClearPacket.ClearMode.MessagesUsers));
SendTo(user, new UsersPopulatePacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new UsersPopulatePacket.ListEntry(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions, true)
user => new UsersPopulatePacket.ListEntry(
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions,
true
)
).OrderByDescending(u => u.Rank).ToArray()));
HandleChannelEventLog(chan.Name, p => SendTo(user, p));
ForceChannel(user, chan);
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name));
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
UserLastChannel[user.UserId] = chan;
if(oldChan != null)
ChannelsUsers.Leave(oldChan, user);
ChannelsUsers.Join(chan, user);
if(oldChan.IsTemporary && oldChan.IsOwner(user))
if(oldChan != null && oldChan.IsTemporary && oldChan.IsOwner(user))
RemoveChannel(oldChan);
}
@ -346,22 +352,21 @@ namespace SharpChat {
public void SendTo(UserInfo user, IServerPacket packet) {
foreach(ConnectionInfo conn in Connections)
if(conn.IsAlive && conn.User == user)
if(conn.IsAuthed && conn.User!.UserId == user.UserId)
conn.Send(packet);
}
public void SendTo(ChannelInfo channel, IServerPacket packet) {
// might be faster to grab the users first and then cascade into that SendTo
IEnumerable<ConnectionInfo> conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel));
foreach(ConnectionInfo conn in conns)
conn.Send(packet);
long[] userIds = ChannelsUsers.GetChannelUserIds(channel);
foreach(ConnectionInfo conn in Connections)
if(conn.IsAuthed && userIds.Contains(conn.User!.UserId))
conn.Send(packet);
}
public void SendToUserChannels(UserInfo user, IServerPacket packet) {
IEnumerable<ChannelInfo> chans = Channels.Values.Where(c => IsInChannel(user, c));
IEnumerable<ConnectionInfo> conns = Connections.Where(conn => conn.IsAuthed && ChannelUsers.Any(cu => cu.UserId == conn.User?.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName))));
foreach(ConnectionInfo conn in conns)
conn.Send(packet);
ChannelInfo[] chans = GetUserChannels(user);
foreach(ChannelInfo chan in chans)
SendTo(chan, packet);
}
public IPAddress[] GetRemoteAddresses(UserInfo user) {
@ -369,10 +374,9 @@ namespace SharpChat {
}
public void ForceChannel(UserInfo user, ChannelInfo? chan = null) {
if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
throw new ArgumentException("no channel???");
SendTo(user, new UserChannelForceJoinPacket(chan.Name));
chan ??= Channels.Get(ChannelsUsers.GetUserLastChannel(user));
if(chan != null)
SendTo(user, new UserChannelForceJoinPacket(chan.Name));
}
public void UpdateChannel(
@ -381,9 +385,6 @@ namespace SharpChat {
int? minRank = null,
string? password = null
) {
if(!Channels.ContainsValue(channel))
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
string prevName = channel.Name;
if(temporary.HasValue)
@ -396,21 +397,21 @@ namespace SharpChat {
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
foreach(UserInfo user in Users.Values.Where(u => u.Rank >= channel.Rank)) {
// the server currently doesn't keep track of what channels a user is already aware of so can't really simulate this yet.
foreach(UserInfo user in Users.GetMany(minRank: channel.Rank))
SendTo(user, new ChannelUpdatePacket(prevName, channel.Name, channel.HasPassword, channel.IsTemporary));
}
}
public void RemoveChannel(ChannelInfo channel) {
if(channel == null || !Channels.Any())
if(channel == null || Channels.PublicCount > 1)
return;
ChannelInfo? defaultChannel = Channels.Values.FirstOrDefault();
ChannelInfo? defaultChannel = Channels.MainChannel;
if(defaultChannel == null)
return;
// Remove channel from the listing
Channels.Remove(channel.Name);
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.
@ -418,7 +419,7 @@ namespace SharpChat {
SwitchChannel(user, defaultChannel, string.Empty);
// Broadcast deletion of channel
foreach(UserInfo user in Users.Values.Where(u => u.Rank >= channel.Rank))
foreach(UserInfo user in Users.GetMany(minRank: channel.Rank))
SendTo(user, new ChannelDeletePacket(channel.Name));
}
}

View file

@ -33,12 +33,12 @@ namespace SharpChat.Commands {
string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0));
if(!ChannelInfo.CheckName(createChanName)) {
if(!SockChatUtility.CheckChannelName(createChanName)) {
ctx.Chat.SendTo(ctx.User, new ChannelNameFormatErrorPacket());
return;
}
if(ctx.Chat.Channels.Values.Any(c => c.NameEquals(createChanName))) {
if(ctx.Chat.Channels.Get(createChanName, SockChatUtility.SanitiseChannelName) != null) {
ctx.Chat.SendTo(ctx.User, new ChannelNameInUseErrorPacket(createChanName));
return;
}
@ -50,8 +50,8 @@ namespace SharpChat.Commands {
ownerId: ctx.User.UserId
);
ctx.Chat.Channels.Add(createChan.Name, createChan);
foreach(UserInfo ccu in ctx.Chat.Users.Values.Where(u => u.Rank >= ctx.Channel.Rank))
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 ChannelCreatePacket(
ctx.Channel.Name,
ctx.Channel.HasPassword,

View file

@ -17,7 +17,7 @@ namespace SharpChat.Commands {
}
string delChanName = string.Join('_', ctx.Args);
ChannelInfo? delChan = ctx.Chat.Channels.Values.FirstOrDefault(c => c.NameEquals(delChanName));
ChannelInfo? delChan = ctx.Chat.Channels.Get(delChanName, SockChatUtility.SanitiseChannelName);
if(delChan == null) {
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorPacket(delChanName));

View file

@ -9,7 +9,7 @@ namespace SharpChat.Commands {
public void Dispatch(UserCommandContext ctx) {
string joinChanStr = ctx.Args.FirstOrDefault() ?? string.Empty;
ChannelInfo? joinChan = ctx.Chat.Channels.Values.FirstOrDefault(c => c.NameEquals(joinChanStr));
ChannelInfo? joinChan = ctx.Chat.Channels.Get(joinChanStr, SockChatUtility.SanitiseChannelName);
if(joinChan == null) {
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorPacket(joinChanStr));

View file

@ -30,13 +30,14 @@ namespace SharpChat.Commands {
int banReasonIndex = 1;
UserInfo? banUser = null;
if(string.IsNullOrEmpty(banUserTarget) || (banUser = ctx.Chat.Users.Values.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) {
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(banUserTarget);
if(string.IsNullOrEmpty(name) || (banUser = ctx.Chat.Users.Get(name: name, nameTarget: target)) == null) {
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorPacket(banUserTarget));
return;
}
if(!ctx.User.IsSuper && banUser.Rank >= ctx.User.Rank && banUser != ctx.User) {
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorPacket(banUser.LegacyName));
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorPacket(SockChatUtility.GetUserName(banUser)));
return;
}
@ -66,7 +67,7 @@ namespace SharpChat.Commands {
MisuzuBanInfo? fbi = await Misuzu.CheckBanAsync(userId, userIp);
if(fbi != null && fbi.IsBanned && !fbi.HasExpired) {
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorPacket(banUser.LegacyName));
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorPacket(SockChatUtility.GetUserName(banUser)));
return;
}

View file

@ -17,7 +17,8 @@ namespace SharpChat.Commands {
}
string whisperUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
UserInfo? whisperUser = ctx.Chat.Users.Values.FirstOrDefault(u => u.NameEquals(whisperUserStr));
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(whisperUserStr);
UserInfo? whisperUser = ctx.Chat.Users.Get(name: name, nameTarget: target);
if(whisperUser == null) {
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorPacket(whisperUserStr));

View file

@ -1,6 +1,5 @@
using SharpChat.Misuzu;
using SharpChat.Packet;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

View file

@ -1,6 +1,5 @@
using SharpChat.Misuzu;
using SharpChat.Packet;
using System;
using System.Linq;
using System.Threading.Tasks;
@ -31,10 +30,11 @@ namespace SharpChat.Commands {
return;
}
UserInfo? unbanUser = ctx.Chat.Users.Values.FirstOrDefault(u => u.NameEquals(unbanUserTarget));
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(unbanUserTarget);
UserInfo? unbanUser = ctx.Chat.Users.Get(name: name, nameTarget: target);
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
unbanUserTargetIsName = false;
unbanUser = ctx.Chat.Users.Values.FirstOrDefault(u => u.UserId == unbanUserId);
unbanUser = ctx.Chat.Users.Get(unbanUserId);
}
if(unbanUser != null)

View file

@ -1,5 +1,4 @@
using SharpChat.Packet;
using System.Linq;
using System.Linq;
namespace SharpChat.Commands {
public class UserAFKCommand : IUserCommand {

View file

@ -19,7 +19,7 @@ namespace SharpChat.Commands {
int offset = 0;
if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) {
targetUser = ctx.Chat.Users.Values.FirstOrDefault(u => u.UserId == targetUserId);
targetUser = ctx.Chat.Users.Get(targetUserId);
++offset;
}
@ -42,7 +42,7 @@ namespace SharpChat.Commands {
else if(string.IsNullOrEmpty(nickStr))
nickStr = string.Empty;
if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Values.Any(u => u.NameEquals(nickStr))) {
if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Get(name: nickStr, nameTarget: UsersContext.NameTarget.UserAndNickName) != null) {
ctx.Chat.SendTo(ctx.User, new UserNameInUseErrorPacket(nickStr));
return;
}

View file

@ -12,13 +12,13 @@ namespace SharpChat.Commands {
if(string.IsNullOrEmpty(channelName)) {
ctx.Chat.SendTo(ctx.User, new WhoServerResponsePacket(
ctx.Chat.Users.Values.Select(u => u.LegacyName).ToArray(),
ctx.User.LegacyName
ctx.Chat.Users.All.Select(u => SockChatUtility.GetUserNameWithStatus(u)).ToArray(),
SockChatUtility.GetUserName(ctx.User)
));
return;
}
ChannelInfo? channel = ctx.Chat.Channels.Values.FirstOrDefault(c => c.NameEquals(channelName));
ChannelInfo? channel = ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName);
if(channel == null) {
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorPacket(channelName));
@ -32,8 +32,8 @@ namespace SharpChat.Commands {
ctx.Chat.SendTo(ctx.User, new WhoChannelResponsePacket(
channel.Name,
ctx.Chat.GetChannelUsers(channel).Select(user => user.LegacyName).ToArray(),
ctx.User.LegacyName
ctx.Chat.GetChannelUsers(channel).Select(user => SockChatUtility.GetUserNameWithStatus(user)).ToArray(),
SockChatUtility.GetUserNameWithStatus(ctx.User)
));
}
}

View file

@ -18,7 +18,8 @@ namespace SharpChat.Commands {
string ipUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
UserInfo? ipUser;
if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.Values.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) {
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(ipUserStr);
if(string.IsNullOrWhiteSpace(name) || (ipUser = ctx.Chat.Users.Get(name: name, nameTarget: target)) == null) {
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorPacket(ipUserStr));
return;
}

View file

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharpChat.Config {
public interface IConfig : IDisposable {

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharpChat.Events {
namespace SharpChat.Events {
public interface IChatEvent {
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace SharpChat.Misuzu {
public class MisuzuAuthInfo {

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat.Packet {
namespace SharpChat.Packet {
public class AuthSuccessPacket : ServerPacket {
private readonly long UserId;
private readonly string UserName;
@ -41,7 +39,7 @@ namespace SharpChat.Packet {
UserPerms.HasFlag(UserPermissions.CreateChannel) ? (
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
) : 0,
ChannelName,
SockChatUtility.SanitiseChannelName(ChannelName),
MaxMessageLength
);
}

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat.Packet {
namespace SharpChat.Packet {
public class ChannelCreatePacket : ServerPacket {
private readonly string ChannelName;
private readonly bool ChannelHasPassword;
@ -19,7 +17,7 @@ namespace SharpChat.Packet {
public override string Pack() {
return string.Format(
"4\t0\t{0}\t{1}\t{2}",
ChannelName,
SockChatUtility.SanitiseChannelName(ChannelName),
ChannelHasPassword ? 1 : 0,
ChannelIsTemporary ? 1 : 0
);

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t0\fcrchan\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t0\fcrchan\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t1\fndchan\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t1\fndchan\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat.Packet {
namespace SharpChat.Packet {
public class ChannelDeletePacket : ServerPacket {
private readonly string ChannelName;
@ -9,7 +7,10 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("4\t2\t{0}", ChannelName);
return string.Format(
"4\t2\t{0}",
SockChatUtility.SanitiseChannelName(ChannelName)
);
}
}
}

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t0\fdelchan\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t0\fdelchan\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t1\fnischan\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t1\fnischan\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t1\fnochan\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t1\fnochan\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t1\fipwchan\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t1\fipwchan\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t1\fipchan\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t1\fipchan\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat.Packet {
namespace SharpChat.Packet {
public class ChannelUpdatePacket : ServerPacket {
private readonly string ChannelNamePrevious;
private readonly string ChannelNameNew;
@ -22,8 +20,8 @@ namespace SharpChat.Packet {
public override string Pack() {
return string.Format(
"4\t1\t{0}\t{1}\t{2}\t{3}",
ChannelNamePrevious,
ChannelNameNew,
SockChatUtility.SanitiseChannelName(ChannelNamePrevious),
SockChatUtility.SanitiseChannelName(ChannelNameNew),
ChannelHasPassword ? 1 : 0,
ChannelIsTemporary ? 1 : 0
);

View file

@ -1,5 +1,4 @@
using System;
using System.Text;
using System.Text;
namespace SharpChat.Packet {
public class ChannelsPopulatePacket : ServerPacket {
@ -19,7 +18,7 @@ namespace SharpChat.Packet {
foreach(ListEntry entry in Entries)
sb.AppendFormat(
"\t{0}\t{1}\t{2}",
entry.Name,
SockChatUtility.SanitiseChannelName(entry.Name),
entry.HasPassword ? 1 : 0,
entry.IsTemporary ? 1 : 0
);

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t1\fcmdna\f/{1}\t{2}\t10010", Timestamp, CommandName, SequenceId);
return string.Format(
"2\t{0}\t-1\t1\fcmdna\f/{1}\t{2}\t10010",
Timestamp,
CommandName,
SequenceId
);
}
}
}

View file

@ -13,7 +13,7 @@ namespace SharpChat.Packet {
public override string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fsay\f{1}\twelcome\t0\t10010",
Timestamp, Utility.Sanitise(Body)
Timestamp, SockChatUtility.SanitiseMessageBody(Body)
);
}
}

View file

@ -24,7 +24,7 @@ namespace SharpChat.Packet {
}
public override string Pack() {
string body = Utility.Sanitise(Body);
string body = SockChatUtility.SanitiseMessageBody(Body);
if(IsAction)
body = string.Format("<i>{0}</i>", body);

View file

@ -14,7 +14,7 @@ namespace SharpChat.Packet {
return string.Format(
"2\t{0}\t-1\t0\fsay\f{1}\t{2}\t10010",
Timestamp,
Utility.Sanitise(Body),
SockChatUtility.SanitiseMessageBody(Body),
SequenceId
);
}

View file

@ -1,5 +1,4 @@
using SharpChat.EventStorage;
using System;
using System.Text;
namespace SharpChat.Packet {
@ -32,7 +31,7 @@ namespace SharpChat.Packet {
sb.AppendFormat(
"{0}\t{1}\t{2}\t{3} {4} {5} {6} {7}",
Event.Sender?.UserId,
Event.Sender?.LegacyNameWithStatus,
Event.Sender == null ? string.Empty : SockChatUtility.GetUserNameWithStatus(Event.Sender),
Event.Sender?.Colour,
Event.Sender?.Rank,
Event.Sender?.Permissions.HasFlag(UserPermissions.KickUser) == true ? 1 : 0,
@ -49,7 +48,7 @@ namespace SharpChat.Packet {
if(isBroadcast)
sb.Append("0\fsay\f");
string body = Utility.Sanitise(Event.Data.RootElement.GetProperty("text").GetString());
string body = SockChatUtility.SanitiseMessageBody(Event.Data.RootElement.GetProperty("text").GetString());
if(isAction)
body = string.Format("<i>{0}</i>", body);
@ -57,11 +56,19 @@ namespace SharpChat.Packet {
break;
case "chan:join":
sb.AppendFormat("{0}\t0\fjchan\f{1}", V1_CHATBOT, Event.Sender?.LegacyName);
sb.AppendFormat(
"{0}\t0\fjchan\f{1}",
V1_CHATBOT,
Event.Sender == null ? string.Empty : SockChatUtility.GetUserName(Event.Sender)
);
break;
case "chan:leave":
sb.AppendFormat("{0}\t0\flchan\f{1}", V1_CHATBOT, Event.Sender?.LegacyName);
sb.AppendFormat(
"{0}\t0\flchan\f{1}",
V1_CHATBOT,
Event.Sender == null ? string.Empty : SockChatUtility.GetUserName(Event.Sender)
);
break;
}

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat.Packet {
namespace SharpChat.Packet {
public class UserChannelForceJoinPacket : ServerPacket {
private readonly string ChannelName;
@ -9,7 +7,10 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("5\t2\t{0}", ChannelName);
return string.Format(
"5\t2\t{0}",
SockChatUtility.SanitiseChannelName(ChannelName)
);
}
}
}

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat.Packet {
namespace SharpChat.Packet {
public class UserChannelJoinPacket : ServerPacket {
private readonly long UserId;
private readonly string UserName;

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat.Packet {
namespace SharpChat.Packet {
public class UserUpdatePacket : ServerPacket {
private readonly long UserId;
private readonly string UserName;

View file

@ -1,5 +1,4 @@
using System;
using System.Text;
using System.Text;
namespace SharpChat.Packet {
public class UsersPopulatePacket : ServerPacket {

View file

@ -11,7 +11,12 @@ namespace SharpChat.Packet {
}
public override string Pack() {
return string.Format("2\t{0}\t-1\t1\fwhoerr\f{1}\t{2}\t10010", Timestamp, ChannelName, SequenceId);
return string.Format(
"2\t{0}\t-1\t1\fwhoerr\f{1}\t{2}\t10010",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName),
SequenceId
);
}
}
}

View file

@ -18,7 +18,11 @@ namespace SharpChat.Packet {
public override string Pack() {
StringBuilder sb = new();
sb.AppendFormat("2\t{0}\t-1\t0\fwhochan\f{1}\f", Timestamp, ChannelName);
sb.AppendFormat(
"2\t{0}\t-1\t0\fwhochan\f{1}\f",
Timestamp,
SockChatUtility.SanitiseChannelName(ChannelName)
);
foreach(string userName in Users) {
sb.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");

View file

@ -1,6 +1,4 @@
using System;
namespace SharpChat {
namespace SharpChat {
public class PacketHandlerContext {
public string Text { get; }
public ChatContext Chat { get; }

View file

@ -16,12 +16,12 @@ namespace SharpChat.PacketHandlers {
public AuthHandler(
MisuzuClient msz,
ChannelInfo defaultChannel,
ChannelInfo? defaultChannel,
CachedValue<int> maxMsgLength,
CachedValue<int> maxConns
) {
Misuzu = msz;
DefaultChannel = defaultChannel;
DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
MaxMessageLength = maxMsgLength;
MaxConnections = maxConns;
}
@ -114,7 +114,7 @@ namespace SharpChat.PacketHandlers {
await ctx.Chat.ContextAccess.WaitAsync();
try {
UserInfo? user = ctx.Chat.Users.Values.FirstOrDefault(u => u.UserId == fai.UserId);
UserInfo? user = ctx.Chat.Users.Get(fai.UserId);
if(user == null)
user = new UserInfo(

View file

@ -31,7 +31,7 @@ namespace SharpChat.PacketHandlers {
ctx.Chat.ContextAccess.Wait();
try {
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
(string, string)[] bumpList = ctx.Chat.Users.Values
(string, string)[] bumpList = ctx.Chat.Users.All
.Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u))
.Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty))
.ToArray();

View file

@ -1,14 +1,10 @@
using SharpChat.Commands;
using SharpChat.Config;
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Packet;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.PacketHandlers
{
namespace SharpChat.PacketHandlers {
public class SendMessageHandler : IPacketHandler {
private readonly CachedValue<int> MaxMessageLength;
@ -47,8 +43,10 @@ namespace SharpChat.PacketHandlers
ctx.Chat.ContextAccess.Wait();
try {
if(!ctx.Chat.UserLastChannel.TryGetValue(user.UserId, out ChannelInfo? channel)
|| !ctx.Chat.IsInChannel(user, channel))
ChannelInfo? channelInfo = ctx.Chat.Channels.Get(
ctx.Chat.ChannelsUsers.GetUserLastChannel(user)
);
if(channelInfo == null)
return;
if(user.Status != UserStatus.Online)
@ -65,7 +63,7 @@ namespace SharpChat.PacketHandlers
#endif
if(messageText.StartsWith("/")) {
UserCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channel);
UserCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channelInfo);
IUserCommand? command = null;
@ -83,7 +81,7 @@ namespace SharpChat.PacketHandlers
ctx.Chat.DispatchEvent(new MessageCreateEvent(
SharpId.Next(),
channel,
channelInfo,
user,
DateTimeOffset.Now,
messageText,

View file

@ -36,8 +36,6 @@ namespace SharpChat {
private bool IsShuttingDown = false;
private ChannelInfo DefaultChannel { get; set; }
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IEventStorage evtStore, IConfig config) {
Logger.Write("Initialising Sock Chat server...");
@ -67,15 +65,13 @@ namespace SharpChat {
rank: channelCfg.SafeReadValue("minRank", 0)
);
Context.Channels.Add(channelInfo.Name, channelInfo);
DefaultChannel ??= channelInfo;
Context.Channels.Add(channelInfo);
}
DefaultChannel ??= new ChannelInfo("Default");
if(Context.Channels.Count < 1)
Context.Channels.Add(DefaultChannel.Name, DefaultChannel);
if(Context.Channels.PublicCount < 1)
Context.Channels.Add(new ChannelInfo("Default"));
GuestHandlers.Add(new AuthHandler(Misuzu, DefaultChannel, MaxMessageLength, MaxConnections));
GuestHandlers.Add(new AuthHandler(Misuzu, Context.Channels.MainChannel, MaxMessageLength, MaxConnections));
AuthedHandlers.AddRange(new IPacketHandler[] {
new PingHandler(Misuzu),

View file

@ -0,0 +1,65 @@
using System;
using System.Text.RegularExpressions;
namespace SharpChat {
public static class SockChatUtility {
private static readonly Regex ChannelName = new(@"[^A-Za-z0-9\-_]", RegexOptions.CultureInvariant | RegexOptions.Compiled);
public static string SanitiseMessageBody(string? body) {
if(string.IsNullOrEmpty(body))
return string.Empty;
return body.Replace("<", "&lt;").Replace(">", "&gt;").Replace("\n", " <br/> ").Replace("\t", " ");
}
public static string SanitiseChannelName(string name) {
return ChannelName.Replace(name.Replace(" ", "_"), "-");
}
public static bool CheckChannelName(string name) {
return name.Length < 1 || ChannelName.IsMatch(name);
}
public static string GetUserName(UserInfo info) {
return string.IsNullOrWhiteSpace(info.NickName) ? info.UserName : $"~{info.NickName}";
}
public static string GetUserNameWithStatus(UserInfo info) {
string name = GetUserName(info);
if(info.Status == UserStatus.Away)
name = string.Format(
"&lt;{0}&gt;_{1}",
info.StatusText[..Math.Min(info.StatusText.Length, 5)].ToUpperInvariant(),
name
);
return name;
}
public static (string, UsersContext.NameTarget) ExplodeUserName(string name) {
UsersContext.NameTarget target = UsersContext.NameTarget.UserName;
if(name.StartsWith("<")) {
int gt = name.IndexOf(">_");
if(gt > 0) {
gt += 2;
name = name[gt..];
}
} else if(name.StartsWith("&lt;")) {
int gt = name.IndexOf("&gt;_");
if(gt > 0) {
gt += 5;
name = name[gt..];
}
}
if(name.StartsWith("~")) {
target = UsersContext.NameTarget.NickName;
name = name[1..];
}
return (name, target);
}
}
}

View file

@ -1,7 +1,4 @@
using System;
using System.Text;
namespace SharpChat {
namespace SharpChat {
public class UserInfo {
public const int DEFAULT_SIZE = 30;
public const int DEFAULT_MINIMUM_DELAY = 10000;
@ -17,21 +14,6 @@ namespace SharpChat {
public UserStatus Status { get; set; }
public string StatusText { get; set; }
public string LegacyName => string.IsNullOrWhiteSpace(NickName) ? UserName : $"~{NickName}";
public string LegacyNameWithStatus {
get {
StringBuilder sb = new();
if(Status == UserStatus.Away)
sb.AppendFormat("&lt;{0}&gt;_", StatusText[..Math.Min(StatusText.Length, 5)].ToUpperInvariant());
sb.Append(LegacyName);
return sb.ToString();
}
}
public UserInfo(
long userId,
string userName,
@ -54,13 +36,6 @@ namespace SharpChat {
IsSuper = isSuper;
}
public bool NameEquals(string? name) {
return string.Equals(name, UserName, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, NickName, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, LegacyName, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, LegacyNameWithStatus, StringComparison.InvariantCultureIgnoreCase);
}
public static string GetDMChannelName(UserInfo user1, UserInfo user2) {
return user1.UserId < user2.UserId
? $"@{user1.UserId}-{user2.UserId}"

112
SharpChat/UsersContext.cs Normal file
View file

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat {
public class UsersContext {
[Flags]
public enum NameTarget {
None = 0,
UserName = 1,
NickName = 2,
UserAndNickName = UserName | NickName,
}
private readonly List<UserInfo> Users = new();
public int TotalCount { get; private set; }
public UserInfo[] All => Users.ToArray();
public UserInfo? Get(
long? userId = null,
string? name = null,
NameTarget nameTarget = NameTarget.UserName
) {
foreach(UserInfo info in Users) {
if(userId != null && info.UserId != userId)
continue;
if(name != null) {
// this could probably all fit in a single if condition, but it'd be massive and disgusting
// and require more thinking power than my goldfish brain can muster
bool match = false;
if(nameTarget.HasFlag(NameTarget.UserName) && info.UserName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
match = true;
else if(nameTarget.HasFlag(NameTarget.NickName) && info.NickName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
match = true;
if(!match)
continue;
}
return info;
}
return null;
}
public UserInfo[] GetMany(
long[]? userIds = null,
string[]? names = null,
NameTarget namesTarget = NameTarget.UserName,
int? minRank = null
) {
List<UserInfo> users = new();
foreach(UserInfo info in Users) {
if(minRank != null && info.Rank < minRank)
continue;
if(userIds != null && !userIds.Contains(info.UserId))
continue;
if(names?.Length > 0) {
bool match = false;
foreach(string name in names) {
if(namesTarget.HasFlag(NameTarget.UserName) && info.UserName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
match = true;
else if(namesTarget.HasFlag(NameTarget.NickName) && info.NickName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
match = true;
}
if(!match)
continue;
}
users.Add(info);
}
return users.ToArray();
}
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));
Users.Add(info);
++TotalCount;
}
public void Remove(UserInfo info) {
if(!Users.Contains(info)) {
Remove(info.UserId);
return;
}
Users.Remove(info);
--TotalCount;
}
public void Remove(
long? userId = null,
string? name = null,
NameTarget nameTarget = NameTarget.UserName
) {
UserInfo? info = Get(userId, name, nameTarget);
if(info == null)
return;
Users.Remove(info);
--TotalCount;
}
}
}

View file

@ -1,10 +0,0 @@
namespace SharpChat {
public static class Utility {
public static string Sanitise(string? body) {
if(string.IsNullOrEmpty(body))
return string.Empty;
return body.Replace("<", "&lt;").Replace(">", "&gt;").Replace("\n", " <br/> ").Replace("\t", " ");
}
}
}