Simplify rate limiter, disabled silencing and merged BasicUser and ChatUser.

This commit is contained in:
flash 2023-02-17 22:47:44 +01:00
parent d268a419dc
commit 546e8a2c83
15 changed files with 98 additions and 115 deletions

View File

@ -80,15 +80,6 @@ namespace SharpChat {
return Users.Where(u => ids.Contains(u.UserId)).ToArray();
}
public void DebugPrintChannelUsers() {
lock(ChannelUsersAccess) {
Logger.Write("DebugPrintChannelUsers()");
foreach(ChannelUserAssoc cua in ChannelUsers)
Logger.Write(cua);
Logger.Write(string.Empty);
}
}
public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(duration > TimeSpan.Zero)
SendTo(user, new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration));
@ -143,7 +134,6 @@ namespace SharpChat {
foreach(ChatChannel chan in channels) {
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));
DebugPrintChannelUsers();
SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason));
Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason));

View File

@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat {
public enum ChatRateLimitState {
None,
Warning,
Kick,
}
public class ChatRateLimiter {
private const int FLOOD_PROTECTION_AMOUNT = 30;
private const int FLOOD_PROTECTION_THRESHOLD = 10;
private readonly Queue<DateTimeOffset> TimePoints = new();
public ChatRateLimitState State {
get {
if(TimePoints.Count == FLOOD_PROTECTION_AMOUNT) {
if((TimePoints.Last() - TimePoints.First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD)
return ChatRateLimitState.Kick;
if((TimePoints.Last() - TimePoints.Skip(5).First()).TotalSeconds <= FLOOD_PROTECTION_THRESHOLD)
return ChatRateLimitState.Warning;
}
return ChatRateLimitState.None;
}
}
public void AddTimePoint(DateTimeOffset? dto = null) {
if(!dto.HasValue)
dto = DateTimeOffset.Now;
if(TimePoints.Count >= FLOOD_PROTECTION_AMOUNT)
TimePoints.Dequeue();
TimePoints.Enqueue(dto.Value);
}
}
}

View File

@ -3,7 +3,7 @@ using System;
using System.Text;
namespace SharpChat {
public class BasicUser : IEquatable<BasicUser> {
public class ChatUser : IEquatable<ChatUser> {
private const int RANK_NO_FLOOD = 9;
public long UserId { get; set; }
@ -18,6 +18,11 @@ namespace SharpChat {
public bool HasFloodProtection
=> Rank < RANK_NO_FLOOD;
public readonly RateLimiter RateLimiter = new();
// This needs to be a session thing
public ChatChannel CurrentChannel { get; set; }
public string DisplayName {
get {
StringBuilder sb = new();
@ -36,6 +41,24 @@ namespace SharpChat {
}
}
public ChatUser() { }
public ChatUser(MisuzuAuthInfo auth) {
UserId = auth.UserId;
ApplyAuth(auth);
}
public void ApplyAuth(MisuzuAuthInfo auth) {
Username = auth.Username;
if(Status == ChatUserStatus.Offline)
Status = ChatUserStatus.Online;
Colour = ChatColour.FromMisuzu(auth.ColourRaw);
Rank = auth.Rank;
Permissions = auth.Permissions;
}
public bool Can(ChatUserPermissions perm, bool strict = false) {
ChatUserPermissions perms = Permissions & perm;
return strict ? perms == perm : perms > 0;
@ -63,56 +86,23 @@ namespace SharpChat {
return sb.ToString();
}
public bool NameEquals(string name) {
return string.Equals(name, Username, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, Nickname, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, DisplayName, StringComparison.InvariantCultureIgnoreCase);
}
public override int GetHashCode() {
return UserId.GetHashCode();
}
public override bool Equals(object obj) {
return Equals(obj as BasicUser);
return Equals(obj as ChatUser);
}
public bool Equals(BasicUser other) {
public bool Equals(ChatUser other) {
return UserId == other?.UserId;
}
}
public class ChatUser : BasicUser {
public DateTimeOffset SilencedUntil { get; set; }
public readonly ChatRateLimiter RateLimiter = new();
// This needs to be a session thing
public ChatChannel CurrentChannel { get; set; }
public bool IsSilenced
=> DateTimeOffset.UtcNow - SilencedUntil <= TimeSpan.Zero;
public ChatUser() {}
public ChatUser(MisuzuAuthInfo auth) {
UserId = auth.UserId;
ApplyAuth(auth, true);
}
public void ApplyAuth(MisuzuAuthInfo auth, bool invalidateRestrictions = false) {
Username = auth.Username;
if(Status == ChatUserStatus.Offline)
Status = ChatUserStatus.Online;
Colour = ChatColour.FromMisuzu(auth.ColourRaw);
Rank = auth.Rank;
Permissions = auth.Permissions;
if(invalidateRestrictions || !IsSilenced)
SilencedUntil = auth.SilencedUntil;
}
public bool NameEquals(string name) {
return string.Equals(name, Username, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, Nickname, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(name, DisplayName, StringComparison.InvariantCultureIgnoreCase);
}
public static string GetDMChannelName(ChatUser user1, ChatUser user2) {
return user1.UserId < user2.UserId

View File

@ -33,7 +33,7 @@ namespace SharpChat.Commands {
return;
}
if(silUser.IsSilenced) {
if(RNG.Next() > 1 /*silUser.IsSilenced*/) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.SILENCE_ALREADY));
return;
}
@ -49,7 +49,7 @@ namespace SharpChat.Commands {
silenceUntil = DateTimeOffset.UtcNow.AddSeconds(silenceSeconds);
}
silUser.SilencedUntil = silenceUntil;
//silUser.SilencedUntil = silenceUntil;
ctx.Chat.SendTo(silUser, new LegacyCommandResponse(LCR.SILENCED, false));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.TARGET_SILENCED, false, silUser.DisplayName));
}

View File

@ -28,12 +28,12 @@ namespace SharpChat.Commands {
return;
}
if(!unsilUser.IsSilenced) {
if(RNG.Next() > 1 /*!unsilUser.IsSilenced*/) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.NOT_SILENCED));
return;
}
unsilUser.SilencedUntil = DateTimeOffset.MinValue;
//unsilUser.SilencedUntil = DateTimeOffset.MinValue;
ctx.Chat.SendTo(unsilUser, new LegacyCommandResponse(LCR.UNSILENCED, false));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.TARGET_UNSILENCED, false, unsilUser.DisplayName));
}

View File

@ -72,7 +72,7 @@ namespace SharpChat.EventStorage {
evt.DateTime = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created"));
if(!reader.IsDBNull(reader.GetOrdinal("event_sender"))) {
evt.Sender = new BasicUser {
evt.Sender = new ChatUser {
UserId = reader.GetInt64("event_sender"),
Username = reader.GetString("event_sender_name"),
Colour = ChatColour.FromMisuzu(reader.GetInt32("event_sender_colour")),

View File

@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
namespace SharpChat.Events {
public class ChatMessage : IChatMessage {
[JsonIgnore]
public BasicUser Sender { get; set; }
public ChatUser Sender { get; set; }
[JsonIgnore]
public string ChannelName { get; set; }

View File

@ -12,7 +12,7 @@ namespace SharpChat.Events {
public interface IChatEvent {
DateTimeOffset DateTime { get; set; }
BasicUser Sender { get; set; }
ChatUser Sender { get; set; }
string ChannelName { get; set; }
ChatMessageFlags Flags { get; set; }
long SequenceId { get; set; }

View File

@ -7,7 +7,7 @@ namespace SharpChat.Events {
public DateTimeOffset DateTime { get; set; }
[JsonIgnore]
public BasicUser Sender { get; set; }
public ChatUser Sender { get; set; }
[JsonIgnore]
public string ChannelName { get; set; }
@ -19,7 +19,7 @@ namespace SharpChat.Events {
public long SequenceId { get; set; }
public UserChannelJoinEvent() { }
public UserChannelJoinEvent(DateTimeOffset joined, BasicUser user, ChatChannel channel) {
public UserChannelJoinEvent(DateTimeOffset joined, ChatUser user, ChatChannel channel) {
DateTime = joined;
Sender = user;
ChannelName = channel.Name;

View File

@ -7,7 +7,7 @@ namespace SharpChat.Events {
public DateTimeOffset DateTime { get; set; }
[JsonIgnore]
public BasicUser Sender { get; set; }
public ChatUser Sender { get; set; }
[JsonIgnore]
public string ChannelName { get; set; }
@ -19,7 +19,7 @@ namespace SharpChat.Events {
public long SequenceId { get; set; }
public UserChannelLeaveEvent() { }
public UserChannelLeaveEvent(DateTimeOffset parted, BasicUser user, ChatChannel channel) {
public UserChannelLeaveEvent(DateTimeOffset parted, ChatUser user, ChatChannel channel) {
DateTime = parted;
Sender = user;
ChannelName = channel.Name;

View File

@ -7,7 +7,7 @@ namespace SharpChat.Events {
public DateTimeOffset DateTime { get; set; }
[JsonIgnore]
public BasicUser Sender { get; set; }
public ChatUser Sender { get; set; }
[JsonIgnore]
public string ChannelName { get; set; }
@ -19,7 +19,7 @@ namespace SharpChat.Events {
public long SequenceId { get; set; }
public UserConnectEvent() { }
public UserConnectEvent(DateTimeOffset joined, BasicUser user, ChatChannel channel) {
public UserConnectEvent(DateTimeOffset joined, ChatUser user, ChatChannel channel) {
DateTime = joined;
Sender = user;
ChannelName = channel.Name;

View File

@ -9,7 +9,7 @@ namespace SharpChat.Events {
public DateTimeOffset DateTime { get; set; }
[JsonIgnore]
public BasicUser Sender { get; set; }
public ChatUser Sender { get; set; }
[JsonIgnore]
public string ChannelName { get; set; }
@ -24,7 +24,7 @@ namespace SharpChat.Events {
public UserDisconnectReason Reason { get; set; }
public UserDisconnectEvent() { }
public UserDisconnectEvent(DateTimeOffset parted, BasicUser user, ChatChannel channel, UserDisconnectReason reason) {
public UserDisconnectEvent(DateTimeOffset parted, ChatUser user, ChatChannel channel, UserDisconnectReason reason) {
DateTime = parted;
Sender = user;
ChannelName = channel.Name;

View File

@ -46,7 +46,7 @@ namespace SharpChat.PacketHandlers {
if(channel == null
|| !ctx.Chat.IsInChannel(user, channel)
|| (user.IsSilenced && !user.Can(ChatUserPermissions.SilenceUser)))
/*|| (user.IsSilenced && !user.Can(ChatUserPermissions.SilenceUser))*/)
return;
if(user.Status != ChatUserStatus.Online) {

45
SharpChat/RateLimiter.cs Normal file
View File

@ -0,0 +1,45 @@
using System;
namespace SharpChat {
public class RateLimiter {
public const int DEFAULT_SIZE = 30;
public const int DEFAULT_MINIMUM_DELAY = 10000;
public const int DEFAULT_RISKY_OFFSET = 5;
private readonly int Size;
private readonly int MinimumDelay;
private readonly int RiskyOffset;
private readonly long[] TimePoints;
public RateLimiter(
int size = DEFAULT_SIZE,
int minDelay = DEFAULT_MINIMUM_DELAY,
int riskyOffset = DEFAULT_RISKY_OFFSET
) {
if(size < 2)
throw new ArgumentException("Size is too small.", nameof(size));
if(minDelay < 1000)
throw new ArgumentException("Minimum delay is inhuman.", nameof(minDelay));
if(riskyOffset != 0) {
if(riskyOffset >= size)
throw new ArgumentException("Risky offset may not be greater or equal to the size.", nameof(riskyOffset));
else if(riskyOffset < 0)
throw new ArgumentException("Risky offset may not be negative.", nameof(riskyOffset));
}
Size = size;
MinimumDelay = minDelay;
RiskyOffset = riskyOffset;
TimePoints = new long[Size];
}
public bool IsRisky => TimePoints[RiskyOffset] != 0 && TimePoints[RiskyOffset + 1] != 0 && TimePoints[RiskyOffset] + MinimumDelay >= TimePoints[Size - 1];
public bool IsExceeded => TimePoints[0] != 0 && TimePoints[1] != 0 && TimePoints[0] + MinimumDelay >= TimePoints[Size - 1];
public void Update() {
for(int i = 1; i < Size; ++i)
TimePoints[i - 1] = TimePoints[i];
TimePoints[Size - 1] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}
}

View File

@ -101,8 +101,8 @@ namespace SharpChat {
new PardonUserCommand(msz),
new PardonAddressCommand(msz),
new BanListCommand(msz),
new SilenceApplyCommand(),
new SilenceRevokeCommand(),
//new SilenceApplyCommand(),
//new SilenceRevokeCommand(),
new RemoteAddressCommand(),
});
@ -182,9 +182,9 @@ namespace SharpChat {
// this doesn't affect non-authed connections?????
if(conn.User is not null && conn.User.HasFloodProtection) {
conn.User.RateLimiter.AddTimePoint();
conn.User.RateLimiter.Update();
if(conn.User.RateLimiter.State == ChatRateLimitState.Kick) {
if(conn.User.RateLimiter.IsExceeded) {
Task.Run(async () => {
TimeSpan duration = TimeSpan.FromSeconds(FloodKickLength);
@ -198,7 +198,7 @@ namespace SharpChat {
Context.BanUser(conn.User, duration, UserDisconnectReason.Flood);
}).Wait();
return;
} else if(conn.User.RateLimiter.State == ChatRateLimitState.Warning)
} else if(conn.User.RateLimiter.IsRisky)
Context.SendTo(conn.User, new FloodWarningPacket());
}