Split status elements out of UserInfo and made user update event based.

This commit is contained in:
flash 2024-05-24 19:17:12 +00:00
parent 2eae48325a
commit 86effa0452
18 changed files with 343 additions and 144 deletions

View file

@ -1,4 +1,5 @@
using System.Linq;
using SharpChat.Events;
using System.Linq;
namespace SharpChat.SockChat.Commands {
public class UserAFKCommand : ISockChatClientCommand {
@ -19,10 +20,10 @@ namespace SharpChat.SockChat.Commands {
statusText = statusText[..MAX_LENGTH].Trim();
}
ctx.Chat.UpdateUser(
ctx.Chat.Events.Dispatch(
"user:status",
ctx.User,
status: UserStatus.Away,
statusText: statusText
new UserStatusUpdateEventData(UserStatus.Away, statusText)
);
}
}

View file

@ -1,4 +1,5 @@
using SharpChat.SockChat.PacketsS2C;
using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C;
using System.Linq;
namespace SharpChat.SockChat.Commands {
@ -39,16 +40,22 @@ namespace SharpChat.SockChat.Commands {
nickStr = string.Empty;
else if(nickStr.Length > 15)
nickStr = nickStr[..15];
else if(string.IsNullOrEmpty(nickStr))
nickStr = string.Empty;
if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Get(name: nickStr, nameTarget: UsersContext.NameTarget.UserAndNickName) != null) {
if(string.IsNullOrWhiteSpace(nickStr))
nickStr = string.Empty;
else if(ctx.Chat.Users.Get(name: nickStr, nameTarget: UsersContext.NameTarget.UserAndNickName) != null) {
ctx.Chat.SendTo(ctx.User, new UserNameInUseErrorS2CPacket(nickStr));
return;
}
string? previousName = targetUser == ctx.User ? (targetUser.NickName ?? targetUser.UserName) : null;
ctx.Chat.UpdateUser(targetUser, nickName: nickStr, silent: previousName == null);
ctx.Chat.Events.Dispatch(
"user:update",
targetUser,
new UserUpdateEventData(
nickName: nickStr,
notify: targetUser.UserId == ctx.User.UserId
)
);
}
}
}

View file

@ -12,7 +12,7 @@ namespace SharpChat.SockChat.Commands {
if(string.IsNullOrEmpty(channelName)) {
ctx.Chat.SendTo(ctx.User, new WhoServerResponseS2CPacket(
ctx.Chat.Users.All.Select(u => SockChatUtility.GetUserNameWithStatus(u)).ToArray(),
ctx.Chat.Users.All.Select(u => SockChatUtility.GetUserName(u, ctx.Chat.UserStatuses.Get(u))).ToArray(),
SockChatUtility.GetUserName(ctx.User)
));
return;
@ -32,8 +32,8 @@ namespace SharpChat.SockChat.Commands {
ctx.Chat.SendTo(ctx.User, new WhoChannelResponseS2CPacket(
channel.Name,
ctx.Chat.GetChannelUsers(channel).Select(user => SockChatUtility.GetUserNameWithStatus(user)).ToArray(),
SockChatUtility.GetUserNameWithStatus(ctx.User)
ctx.Chat.GetChannelUsers(channel).Select(user => SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user))).ToArray(),
SockChatUtility.GetUserName(ctx.User, ctx.Chat.UserStatuses.Get(ctx.User))
));
}
}

View file

@ -1,4 +1,5 @@
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System;
@ -121,7 +122,7 @@ namespace SharpChat.SockChat.PacketsC2S {
try {
UserInfo? user = ctx.Chat.Users.Get(fai.UserId);
if(user == null)
if(user == null) {
user = new UserInfo(
fai.UserId,
fai.UserName ?? string.Empty,
@ -130,15 +131,26 @@ namespace SharpChat.SockChat.PacketsC2S {
fai.Permissions,
isSuper: fai.IsSuper
);
else
ctx.Chat.UpdateUser(
user,
userName: fai.UserName,
colour: fai.Colour,
rank: fai.Rank,
perms: fai.Permissions,
isSuper: fai.IsSuper
);
} else {
string? updName = !user.UserName.Equals(fai.UserName) ? fai.UserName : null;
int? updColour = (updColour = fai.Colour.ToMisuzu()) != user.Colour.ToMisuzu() ? updColour : null;
int? updRank = user.Rank != fai.Rank ? fai.Rank : null;
UserPermissions? updPerms = user.Permissions != fai.Permissions ? fai.Permissions : null;
bool? updSuper = user.IsSuper != fai.IsSuper ? fai.IsSuper : null;
if(updName != null || updColour != null || updRank != null || updPerms != null || updSuper != null)
ctx.Chat.Events.Dispatch(
"user:update",
user,
new UserUpdateEventData(
name: updName,
colour: updColour,
rank: updRank,
perms: updPerms,
isSuper: updSuper
)
);
}
// Enforce a maximum amount of connections per user
if(ctx.Chat.Connections.GetCountForUser(user) >= MaxConnections) {

View file

@ -35,7 +35,7 @@ namespace SharpChat.SockChat.PacketsC2S {
List<(string, string)> bumpList = new();
foreach(UserInfo userInfo in ctx.Chat.Users.All) {
if(userInfo.Status != UserStatus.Online)
if(ctx.Chat.UserStatuses.GetStatus(userInfo) != UserStatus.Online)
continue;
string[] remoteAddrs = ctx.Chat.Connections.GetUserRemoteAddresses(userInfo);

View file

@ -49,8 +49,12 @@ namespace SharpChat.SockChat.PacketsC2S {
if(channelInfo == null)
return;
if(user.Status != UserStatus.Online)
ctx.Chat.UpdateUser(user, status: UserStatus.Online);
if(ctx.Chat.UserStatuses.GetStatus(user) != UserStatus.Online)
ctx.Chat.Events.Dispatch(
"user:status",
user,
new UserStatusUpdateEventData(UserStatus.Online)
);
int maxMsgLength = MaxMessageLength;
if(messageText.Length > maxMsgLength)

View file

@ -0,0 +1,32 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserNickChangeLogS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string PrevName;
private readonly string NewName;
public UserNickChangeLogS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string prevName,
string newName
) {
MessageId = messageId;
TimeStamp = timeStamp;
PrevName = prevName;
NewName = newName;
}
public string Pack() {
return string.Format(
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fnick\f{1}\f{2}\t{3}\t0\t10010",
TimeStamp.ToUnixTimeSeconds(),
PrevName,
NewName,
MessageId
);
}
}
}

View file

@ -1,16 +1,21 @@
using System;
namespace SharpChat.SockChat.PacketsS2C {
public class UserUpdateNotificationS2CPacket : ISockChatS2CPacket {
public class UserNickChangeS2CPacket : ISockChatS2CPacket {
private readonly long MessageId;
private readonly DateTimeOffset TimeStamp;
private readonly string PreviousName;
private readonly string PrevName;
private readonly string NewName;
public UserUpdateNotificationS2CPacket(string previousName, string newName) {
MessageId = SharpId.Next();
TimeStamp = DateTimeOffset.UtcNow;
PreviousName = previousName;
public UserNickChangeS2CPacket(
long messageId,
DateTimeOffset timeStamp,
string prevName,
string newName
) {
MessageId = messageId;
TimeStamp = timeStamp;
PrevName = prevName;
NewName = newName;
}
@ -18,7 +23,7 @@ namespace SharpChat.SockChat.PacketsS2C {
return string.Format(
"2\t{0}\t-1\t0\fnick\f{1}\f{2}\t{3}\t10010",
TimeStamp.ToUnixTimeSeconds(),
PreviousName,
PrevName,
NewName,
MessageId
);

View file

@ -11,11 +11,12 @@ namespace SharpChat {
public class SockChatContext : IChatEventHandler {
public readonly SemaphoreSlim ContextAccess = new(1, 1);
public ChannelsContext Channels { get; } = new();
public ConnectionsContext Connections { get; } = new();
public UsersContext Users { get; } = new();
public IEventStorage EventStorage { get; }
public ChatEventDispatcher Events { get; } = new();
public ConnectionsContext Connections { get; } = new();
public ChannelsContext Channels { get; } = new();
public UsersContext Users { get; } = new();
public UserStatusContext UserStatuses { get; } = new();
public ChannelsUsersContext ChannelsUsers { get; } = new();
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
@ -28,6 +29,7 @@ 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))
ChannelsUsers.SetUserLastChannel(info.SenderId, info.ChannelName);
@ -40,7 +42,7 @@ namespace SharpChat {
info.Id,
info.Created,
info.SenderId,
userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
SockChatUtility.GetUserName(info, userStatusInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
@ -49,7 +51,8 @@ namespace SharpChat {
case "user:disconnect":
if(userInfo != null)
UpdateUser(userInfo, status: UserStatus.Offline);
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));
@ -60,7 +63,7 @@ namespace SharpChat {
info.Id,
info.Created,
info.SenderId,
userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
SockChatUtility.GetUserName(info, userStatusInfo),
info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave
);
@ -73,6 +76,86 @@ namespace SharpChat {
}
break;
case "user:status":
if(info.Data is not UserStatusUpdateEventData userStatusUpdate)
break;
if(userStatusInfo.Status == userStatusUpdate.Status
&& userStatusInfo.Text.Equals(userStatusUpdate.Text))
break;
userStatusInfo = UserStatuses.Set(
info.SenderId,
userStatusUpdate.Status,
userStatusUpdate.Text ?? string.Empty
);
SendToUserChannels(info.SenderId, new UserUpdateS2CPacket(
info.SenderId,
SockChatUtility.GetUserName(info, userStatusInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
));
break;
case "user:update":
if(info.Data is not UserUpdateEventData userUpdate || userInfo is null)
break;
bool uuHasChanged = false;
string? uuPrevName = null;
if(userUpdate.Name != null && !userUpdate.Name.Equals(userInfo.UserName)) {
userInfo.UserName = userUpdate.Name;
uuHasChanged = true;
}
if(userUpdate.NickName != null && !userUpdate.NickName.Equals(userInfo.NickName)) {
if(userUpdate.Notify)
uuPrevName = string.IsNullOrWhiteSpace(userInfo.NickName) ? userInfo.UserName : userInfo.NickName;
userInfo.NickName = userUpdate.NickName;
uuHasChanged = true;
}
if(userUpdate.Colour.HasValue && userUpdate.Colour != userInfo.Colour.ToMisuzu()) {
userInfo.Colour = Colour.FromMisuzu(userUpdate.Colour.Value);
uuHasChanged = true;
}
if(userUpdate.Rank != null && userUpdate.Rank != userInfo.Rank) {
userInfo.Rank = userUpdate.Rank.Value;
uuHasChanged = true;
}
if(userUpdate.Perms.HasValue && userUpdate.Perms != userInfo.Permissions) {
userInfo.Permissions = userUpdate.Perms.Value;
uuHasChanged = true;
}
if(userUpdate.IsSuper.HasValue && userUpdate.IsSuper != userInfo.IsSuper)
userInfo.IsSuper = userUpdate.IsSuper.Value;
if(uuHasChanged) {
if(uuPrevName != null)
SendToUserChannels(info.SenderId, new UserNickChangeS2CPacket(
info.Id,
info.Created,
string.IsNullOrWhiteSpace(info.SenderNickName) ? uuPrevName : $"~{info.SenderNickName}",
SockChatUtility.GetUserName(userInfo, userStatusInfo)
));
SendToUserChannels(info.SenderId, new UserUpdateS2CPacket(
userInfo.UserId,
SockChatUtility.GetUserName(userInfo, userStatusInfo),
userInfo.Colour,
userInfo.Rank,
userInfo.Permissions
));
}
break;
case "user:kickban":
if(info.Data is not UserKickBanEventData userBaka)
break;
@ -108,7 +191,7 @@ namespace SharpChat {
case "chan:join":
SendTo(info.ChannelName, new UserChannelJoinS2CPacket(
info.SenderId,
userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
SockChatUtility.GetUserName(info, userStatusInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
@ -219,86 +302,10 @@ namespace SharpChat {
}
}
public ChannelInfo[] GetUserChannels(UserInfo user) {
return Channels.GetMany(ChannelsUsers.GetUserChannelNames(user));
}
public UserInfo[] GetChannelUsers(ChannelInfo channel) {
return Users.GetMany(ChannelsUsers.GetChannelUserIds(channel));
}
public void UpdateUser(
UserInfo user,
string? userName = null,
string? nickName = null,
Colour? colour = null,
UserStatus? status = null,
string? statusText = null,
int? rank = null,
UserPermissions? perms = null,
bool? isSuper = null,
bool silent = false
) {
bool hasChanged = false;
string? previousName = null;
if(userName != null && !user.UserName.Equals(userName)) {
user.UserName = userName;
hasChanged = true;
}
if(nickName != null && !user.NickName.Equals(nickName)) {
if(!silent)
previousName = string.IsNullOrWhiteSpace(user.NickName) ? user.UserName : user.NickName;
user.NickName = nickName;
hasChanged = true;
}
if(colour.HasValue && !user.Colour.Equals(colour.Value)) {
user.Colour = colour.Value;
hasChanged = true;
}
if(status.HasValue && user.Status != status.Value) {
user.Status = status.Value;
hasChanged = true;
}
if(statusText != null && !user.StatusText.Equals(statusText)) {
user.StatusText = statusText;
hasChanged = true;
}
if(rank != null && user.Rank != rank) {
user.Rank = (int)rank;
hasChanged = true;
}
if(perms.HasValue && user.Permissions != perms) {
user.Permissions = perms.Value;
hasChanged = true;
}
if(isSuper.HasValue && user.IsSuper != isSuper) {
user.IsSuper = isSuper.Value;
hasChanged = true;
}
if(hasChanged) {
if(previousName != null)
SendToUserChannels(user, new UserUpdateNotificationS2CPacket(previousName, SockChatUtility.GetUserNameWithStatus(user)));
SendToUserChannels(user, new UserUpdateS2CPacket(
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
user.Colour,
user.Rank,
user.Permissions
));
}
}
public void HandleChannelEventLog(string channelName, Action<ISockChatS2CPacket> handler) {
foreach(ChatEventInfo info in EventStorage.GetChannelEventLog(channelName)) {
switch(info.Type) {
@ -339,6 +346,16 @@ namespace SharpChat {
));
break;
case "user:update":
if(info.Data is UserUpdateEventData userUpdate && userUpdate.Notify)
handler(new UserNickChangeLogS2CPacket(
info.Id,
info.Created,
info.SenderNickName == null ? info.SenderName : $"~{info.SenderNickName}",
userUpdate.NickName == null ? info.SenderName : $"~{userUpdate.NickName}"
));
break;
case "chan:join":
handler(new UserChannelJoinLogS2CPacket(
info.Id,
@ -362,9 +379,11 @@ namespace SharpChat {
if(!ChannelsUsers.Has(chan, user))
Events.Dispatch("user:connect", chan, user);
UserStatusInfo statusInfo = UserStatuses.Get(user);
conn.Send(new AuthSuccessS2CPacket(
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
SockChatUtility.GetUserName(user, statusInfo),
user.Colour,
user.Rank,
user.Permissions,
@ -374,7 +393,7 @@ namespace SharpChat {
conn.Send(new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new UsersPopulateS2CPacket.ListEntry(
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
SockChatUtility.GetUserName(user, statusInfo),
user.Colour,
user.Rank,
user.Permissions,
@ -430,7 +449,7 @@ namespace SharpChat {
SendTo(user, new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select(
user => new UsersPopulateS2CPacket.ListEntry(
user.UserId,
SockChatUtility.GetUserNameWithStatus(user),
SockChatUtility.GetUserName(user, UserStatuses.Get(user)),
user.Colour,
user.Rank,
user.Permissions,
@ -494,7 +513,11 @@ namespace SharpChat {
}
public void SendToUserChannels(UserInfo user, ISockChatS2CPacket packet) {
ChannelInfo[] chans = GetUserChannels(user);
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);

View file

@ -21,25 +21,30 @@ namespace SharpChat.SockChat {
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 GetUserName(UserInfo info, UserStatusInfo? statusInfo = null) {
string name = string.IsNullOrWhiteSpace(info.NickName) ? info.UserName : $"~{info.NickName}";
public static string GetUserNameWithStatus(UserInfo info) {
string name = GetUserName(info);
if(info.Status == UserStatus.Away)
if(statusInfo?.Status == UserStatus.Away)
name = string.Format(
"&lt;{0}&gt;_{1}",
info.StatusText[..Math.Min(info.StatusText.Length, 5)].ToUpperInvariant(),
statusInfo.Text[..Math.Min(statusInfo.Text.Length, 5)].ToUpperInvariant(),
name
);
return name;
}
public static string GetUserName(ChatEventInfo info) {
return string.IsNullOrWhiteSpace(info.SenderNickName) ? info.SenderName : $"~{info.SenderNickName}";
public static string GetUserName(ChatEventInfo info, UserStatusInfo? statusInfo = null) {
string name = string.IsNullOrWhiteSpace(info.SenderNickName) ? info.SenderName : $"~{info.SenderNickName}";
if(statusInfo?.Status == UserStatus.Away)
name = string.Format(
"&lt;{0}&gt;_{1}",
statusInfo.Text[..Math.Min(statusInfo.Text.Length, 5)].ToUpperInvariant(),
name
);
return name;
}
public static (string, UsersContext.NameTarget) ExplodeUserName(string name) {

View file

@ -3,7 +3,6 @@ using SharpChat.Events;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Text.Json;
namespace SharpChat.EventStorage {
@ -91,7 +90,7 @@ namespace SharpChat.EventStorage {
reader.GetInt64("event_id"),
eventType,
DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")),
reader.GetString("event_channel"),
reader.IsDBNull(reader.GetOrdinal("event_channel")) ? string.Empty : reader.GetString("event_channel"),
reader.IsDBNull(reader.GetOrdinal("event_sender")) ? -1 : reader.GetInt64("event_sender"),
reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"),
reader.IsDBNull(reader.GetOrdinal("event_sender_colour")) ? Colour.None : Colour.FromMisuzu(reader.GetInt32("event_sender_colour")),

View file

@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace SharpChat.Events {
[ChatEventDataFor("user:status")]
public class UserStatusUpdateEventData : ChatEventData {
[JsonPropertyName("status")]
public UserStatus Status { get; }
[JsonPropertyName("text")]
public string? Text { get; }
public UserStatusUpdateEventData(UserStatus status, string? text = null) {
Status = status;
Text = string.IsNullOrWhiteSpace(text) ? null : text;
}
}
}

View file

@ -0,0 +1,45 @@
using System.Text.Json.Serialization;
namespace SharpChat.Events {
[ChatEventDataFor("user:update")]
public class UserUpdateEventData : ChatEventData {
[JsonPropertyName("notify")]
public bool Notify { get; }
[JsonPropertyName("name")]
public string? Name { get; }
[JsonPropertyName("colour")]
public int? Colour { get; }
[JsonPropertyName("rank")]
public int? Rank { get; }
[JsonPropertyName("perms")]
public UserPermissions? Perms { get; }
[JsonPropertyName("nick")]
public string? NickName { get; }
[JsonPropertyName("super")]
public bool? IsSuper { get; }
public UserUpdateEventData(
string? name = null,
int? colour = null,
int? rank = null,
UserPermissions? perms = null,
string? nickName = null,
bool? isSuper = null,
bool notify = false
) {
Notify = notify;
Name = name;
Colour = colour;
Rank = rank;
Perms = perms;
NickName = nickName;
IsSuper = isSuper;
}
}
}

View file

@ -11,8 +11,6 @@
public UserPermissions Permissions { get; set; }
public bool IsSuper { get; set; }
public string NickName { get; set; }
public UserStatus Status { get; set; }
public string StatusText { get; set; }
public UserInfo(
long userId,
@ -21,8 +19,6 @@
int rank,
UserPermissions perms,
string? nickName = null,
UserStatus status = UserStatus.Online,
string? statusText = null,
bool isSuper = false
) {
UserId = userId;
@ -31,8 +27,6 @@
Rank = rank;
Permissions = perms;
NickName = nickName ?? string.Empty;
Status = status;
StatusText = statusText ?? string.Empty;
IsSuper = isSuper;
}

View file

@ -1,7 +1,8 @@
namespace SharpChat {
public enum UserStatus {
Online,
Away,
Offline,
public enum UserStatus : int {
Unknown = 0,
Online = 1,
Away = 2,
Offline = 3,
}
}

View file

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace SharpChat {
public class UserStatusContext {
private readonly Dictionary<long, UserStatusInfo> Statuses = new();
public UserStatusInfo Get(long userId) {
return Statuses.ContainsKey(userId)
? Statuses[userId]
: UserStatusInfo.EmptyInstance;
}
public UserStatusInfo Get(UserInfo userInfo) {
return Get(userInfo.UserId);
}
public UserStatus GetStatus(long userId) {
return Get(userId).Status;
}
public UserStatus GetStatus(UserInfo userInfo) {
return Get(userInfo.UserId).Status;
}
public UserStatusInfo Set(long userId, UserStatus status, string text) {
UserStatusInfo statusInfo = new(status, text);
if(Statuses.ContainsKey(userId))
Statuses[userId] = statusInfo;
else
Statuses.Add(userId, statusInfo);
return statusInfo;
}
public void Clear(long userId) {
Statuses.Remove(userId);
}
}
}

View file

@ -0,0 +1,16 @@
namespace SharpChat {
public class UserStatusInfo {
public UserStatus Status { get; }
public string Text { get; }
public UserStatusInfo(
UserStatus status,
string text
) {
Status = status;
Text = text;
}
public static readonly UserStatusInfo EmptyInstance = new(UserStatus.Unknown, string.Empty);
}
}

View file

@ -1,5 +1,4 @@
using SharpChat.SockChat;
using System;
using System;
using System.Collections.Generic;
using System.Linq;