Compare commits

..

10 commits

416 changed files with 3952 additions and 15171 deletions

7
.gitignore vendored
View file

@ -1,7 +1,12 @@
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
SharpChat.Common/version.txt welcome.txt
mariadb.txt
login_key.txt
http-motd.txt
_webdb.txt
msz_url.txt
# User-specific files # User-specific files
*.suo *.suo

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "hamakaze"]
path = hamakaze
url = https://git.flash.moe/flash/hamakaze.git

View file

@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\hamakaze\Hamakaze\Hamakaze.csproj" />
</ItemGroup>
</Project>

View file

@ -1,147 +0,0 @@
using Hamakaze;
using System;
using System.Threading;
using static System.Console;
namespace HttpClientTest {
public static class Program {
public static void Main(string[] args) {
ResetColor();
HttpClient.Instance.DefaultUserAgent = @"SharpChat/1.0";
/*string[] commonMediaTypes = new[] {
@"application/x-executable",
@"application/graphql",
@"application/javascript",
@"application/x.fwif",
@"application/json",
@"application/ld+json",
@"application/msword",
@"application/pdf",
@"application/sql",
@"application/vnd.api+json",
@"application/vnd.ms-excel",
@"application/vnd.ms-powerpoint",
@"application/vnd.oasis.opendocument.text",
@"application/vnd.openxmlformats-officedocument.presentationml.presentation",
@"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@"application/x-www-form-urlencoded",
@"application/xml",
@"application/zip",
@"application/zstd",
@"audio/mpeg",
@"audio/ogg",
@"image/gif",
@"image/apng",
@"image/flif",
@"image/webp",
@"image/x-mng",
@"image/jpeg",
@"image/png",
@"multipart/form-data",
@"text/css",
@"text/csv",
@"text/html",
@"text/php",
@"text/plain",
@"text/xml",
@"text/html; charset=utf-8",
};
Logger.Write(@"Testing Media Type parsing...");
foreach(string mts in commonMediaTypes) {
HttpMediaType hmt = HttpMediaType.Parse(mts);
Logger.Write($@"O {mts}");
Logger.Write($@"P {hmt}");
}
return;*/
static void setForeground(ConsoleColor color) {
ResetColor();
ForegroundColor = color;
}
using ManualResetEvent mre = new(false);
bool kill = false;
string[] urls = {
@"https://flashii.net/",
@"https://flashii.net/changelog",
@"https://abyss.flash.moe/",
@"https://flashii.net/info/contact",
@"https://flashii.net/news/",
@"https://flash.moe/",
@"https://flashii.net/forum/",
};
foreach(string url in urls) {
// routine lifted out of satori
string paramUrl = Uri.EscapeDataString(url);
HttpClient.Send(
new HttpRequestMessage(HttpRequestMessage.GET, $@"https://mii.flashii.net/metadata?url={paramUrl}"),
onComplete: (task, res) => {
WriteLine($@"Connection: {task.Request.Connection}");
WriteLine($@"AcceptEncodings: {string.Join(@", ", task.Request.AcceptedEncodings)}");
WriteLine($@"IsSecure: {task.Request.IsSecure}");
WriteLine($@"RequestTarget: {task.Request.RequestTarget}");
WriteLine($@"UserAgent: {task.Request.UserAgent}");
WriteLine($@"ContentType: {task.Request.ContentType}");
WriteLine();
setForeground(ConsoleColor.Green);
WriteLine($@"Connection: {res.StatusCode}");
WriteLine($@"Connection: {res.StatusMessage}");
WriteLine($@"Connection: {res.Connection}");
WriteLine($@"ContentEncodings: {string.Join(@", ", res.ContentEncodings)}");
WriteLine($@"TransferEncodings: {string.Join(@", ", res.TransferEncodings)}");
WriteLine($@"Date: {res.Date}");
WriteLine($@"Server: {res.Server}");
WriteLine($@"ContentType: {res.ContentType}");
WriteLine();
/*if(res.HasBody) {
string line;
using StreamWriter sw = new StreamWriter(@"out.html", false, new UTF8Encoding(false));
using StreamReader sr = new StreamReader(res.Body, new UTF8Encoding(false), false, leaveOpen: true);
while((line = sr.ReadLine()) != null) {
//Logger.Debug(line);
sw.WriteLine(line);
}
}*/
},
onError: (task, ex) => {
setForeground(ConsoleColor.Red);
WriteLine(ex);
},
onCancel: task => {
setForeground(ConsoleColor.Yellow);
WriteLine(@"Cancelled.");
},
onDownloadProgress: (task, p, t) => {
setForeground(ConsoleColor.Blue);
WriteLine($@"Downloaded {p} bytes of {t} bytes.");
},
onUploadProgress: (task, p, t) => {
setForeground(ConsoleColor.Magenta);
WriteLine($@"Uploaded {p} bytes of {t} bytes.");
},
onStateChange: (task, s) => {
setForeground(ConsoleColor.White);
WriteLine($@"State changed: {s}");
if(!kill && (task.IsFinished || task.IsCancelled)) {
kill = true;
mre?.Set();
}
}
);
}
mre.WaitOne();
ResetColor();
}
}
}

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019-2022 flashwave <me@flash.moe> Copyright (c) 2019-2022 flashwave
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpChat.Common\SharpChat.Common.csproj" />
<ProjectReference Include="..\SharpChat.DataProvider.Misuzu\SharpChat.DataProvider.Misuzu.csproj" />
</ItemGroup>
</Project>

View file

@ -1,99 +0,0 @@
using Hamakaze;
using SharpChat.Bans;
using SharpChat.Configuration;
using SharpChat.DataProvider;
using SharpChat.DataProvider.Misuzu;
using SharpChat.Users.Remote;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using static System.Console;
namespace MisuzuDataProviderTest {
public static class Program {
public static void Main() {
WriteLine("Misuzu Authentication Tester");
using ManualResetEvent mre = new(false);
string cfgPath = Path.GetDirectoryName(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
string buildMode = Path.GetFileName(cfgPath);
cfgPath = Path.Combine(
Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(cfgPath))),
@"SharpChat", @"bin", buildMode, @"net5.0", @"sharpchat.cfg"
);
WriteLine($@"Reading config from {cfgPath}");
using IConfig config = new StreamConfig(cfgPath);
WriteLine($@"Enter token found on {config.ReadValue(@"dp:misuzu:endpoint")}/login:");
string[] token = ReadLine().Split(new[] { '_' }, 2);
HttpClient.Instance.DefaultUserAgent = @"SharpChat/1.0";
IDataProvider dataProvider = new MisuzuDataProvider(config.ScopeTo(@"dp:misuzu"), HttpClient.Instance);
long userId = long.Parse(token[0]);
IPAddress remoteAddr = IPAddress.Parse(@"1.2.4.8");
IUserAuthResponse authRes = null;
mre.Reset();
dataProvider.UserClient.AuthenticateUser(
new UserAuthRequest(userId, token[1], remoteAddr),
onSuccess: res => {
authRes = res;
WriteLine(@"Auth success!");
WriteLine($@" User ID: {authRes.UserId}");
WriteLine($@" Username: {authRes.UserName}");
WriteLine($@" Colour: {authRes.Colour.Raw:X8}");
WriteLine($@" Hierarchy: {authRes.Rank}");
WriteLine($@" Silenced: {authRes.SilencedUntil}");
WriteLine($@" Perms: {authRes.Permissions}");
mre.Set();
},
onFailure: ex => {
WriteLine($@"Auth failed: {ex.Message}");
mre.Set();
}
);
mre.WaitOne();
if(authRes == null)
return;
#if FUCKED
WriteLine(@"Bumping last seen...");
mre.Reset();
dataProvider.UserBumpClient.SubmitBumpUsers(
new[] { new User(authRes) },
onSuccess: () => mre.Set(),
onFailure: ex => {
WriteLine($@"Bump failed: {ex.Message}");
mre.Set();
}
);
mre.WaitOne();
#endif
WriteLine(@"Fetching ban list...");
IEnumerable<IBanRecord> bans = Enumerable.Empty<IBanRecord>();
mre.Reset();
dataProvider.BanClient.GetBanList(x => { bans = x; mre.Set(); }, e => { WriteLine(e); mre.Set(); });
mre.WaitOne();
WriteLine($@"{bans.Count()} BANS");
foreach(IBanRecord ban in bans) {
WriteLine($@"BAN INFO");
WriteLine($@" User ID: {ban.UserId}");
WriteLine($@" Username: {ban.UserName}");
WriteLine($@" IP Address: {ban.UserIP}");
WriteLine($@" Expires: {ban.Expires}");
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,4 @@
/_/ /_/
``` ```
Welcome to the repository of the temporary Flashii chat server. SharpChat is an event based chat server supporting multiple protocols (currently Sock Chat and IRC). Welcome to the repository of the temporary Flashii chat server. This is a reimplementation of the old [PHP based Sock Chat server](https://github.com/flashwave/mahou-chat/) in C#.
> Formerly [PHP Sock Chat](https://github.com/flashwave/mahou-chat/) but without PHP but with C# but also with multiple sessions

View file

@ -1,344 +0,0 @@
using SharpChat.Events;
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Net;
namespace SharpChat.Bans {
public class BanManager {
private UserManager Users { get; }
private IBanClient BanClient { get; }
private IRemoteUserClient RemoteUserClient { get; }
private IEventDispatcher Dispatcher { get; }
private readonly object Sync = new();
public BanManager(
UserManager users,
IBanClient banClient,
IRemoteUserClient remoteUserClient,
IEventDispatcher dispatcher
) {
Users = users ?? throw new ArgumentNullException(nameof(users));
BanClient = banClient ?? throw new ArgumentNullException(nameof(banClient));
RemoteUserClient = remoteUserClient ?? throw new ArgumentNullException(nameof(remoteUserClient));
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
public void GetBanList(
Action<IEnumerable<IBanRecord>> onSuccess,
Action<Exception> onFailure
) {
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.GetBanList(onSuccess, onFailure);
}
public void CheckBan(
long userId,
IPAddress ipAddress,
Action<IBanRecord> onSuccess,
Action<Exception> onFailure
) {
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
userId,
rui => {
if(rui == null)
onSuccess(null);
else
CheckBan(rui, ipAddress, onSuccess, onFailure);
},
onFailure
);
}
public void CheckBan(
IUser localUserInfo,
IPAddress ipAddress,
Action<IBanRecord> onSuccess,
Action<Exception> onFailure
) {
if(localUserInfo == null)
throw new ArgumentNullException(nameof(localUserInfo));
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localUserInfo,
rui => {
if(rui == null)
onSuccess(null);
else
CheckBan(rui, ipAddress, onSuccess, onFailure);
},
onFailure
);
}
public void CheckBan(
IRemoteUser remoteUserInfo,
IPAddress ipAddress,
Action<IBanRecord> onSuccess,
Action<Exception> onFailure
) {
if(remoteUserInfo == null)
throw new ArgumentNullException(nameof(remoteUserInfo));
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.CheckBan(remoteUserInfo, ipAddress, onSuccess, onFailure);
}
public void CreateBan(
string subjectName,
IUser localModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(subjectName == null)
throw new ArgumentNullException(nameof(subjectName));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
subjectName,
remoteSubject => {
if(remoteSubject == null)
onSuccess(false);
else
CreateBan(remoteSubject, localModerator, permanent, duration, reason, onSuccess, onFailure);
},
onFailure
);
}
public void CreateBan(
IUser localSubject,
IUser localModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(localSubject == null)
throw new ArgumentNullException(nameof(localSubject));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localSubject,
remoteSubject => {
if(remoteSubject == null)
onSuccess(false);
else
CreateBan(remoteSubject, localModerator, permanent, duration, reason, onSuccess, onFailure);
},
onFailure
);
}
public void CreateBan(
IRemoteUser remoteSubject,
IUser localModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(remoteSubject == null)
throw new ArgumentNullException(nameof(remoteSubject));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localModerator,
remoteModerator => CreateBan(remoteSubject, remoteModerator, permanent, duration, reason, onSuccess, onFailure),
onFailure
);
}
public void CreateBan(
IRemoteUser remoteSubject,
IRemoteUser remoteModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(remoteSubject == null)
throw new ArgumentNullException(nameof(remoteSubject));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.CreateBan(remoteSubject, remoteModerator, permanent, duration, reason, success => {
Dispatcher.DispatchEvent(this, new UserBanCreatedEvent(remoteSubject, remoteModerator, permanent, duration, reason));
Users.Disconnect(
remoteSubject,
remoteModerator == null
? UserDisconnectReason.Flood
: UserDisconnectReason.Kicked
);
onSuccess.Invoke(success);
}, onFailure);
}
public void RemoveBan(
long userId,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
userId,
remoteUser => {
if(remoteUser == null)
onSuccess(false);
else
RemoveBan(remoteUser, onSuccess, onFailure);
},
onFailure
);
}
public void RemoveBan(
string userName,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(userName == null)
throw new ArgumentNullException(nameof(userName));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
userName,
remoteUser => {
if(remoteUser == null)
onSuccess(false);
else
RemoveBan(remoteUser, onSuccess, onFailure);
},
onFailure
);
}
public void RemoveBan(
IUser localUser,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(localUser == null)
throw new ArgumentNullException(nameof(localUser));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localUser,
remoteUser => {
if(remoteUser == null)
onSuccess(false);
else
RemoveBan(remoteUser, onSuccess, onFailure);
},
onFailure
);
}
public void RemoveBan(
IRemoteUser remoteUser,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(remoteUser == null)
throw new ArgumentNullException(nameof(remoteUser));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.RemoveBan(remoteUser, success => {
Dispatcher.DispatchEvent(this, new UserBanRemovedEvent(remoteUser));
onSuccess.Invoke(success);
}, onFailure);
}
public void RemoveBan(
IPAddress ipAddress,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.RemoveBan(ipAddress, success => {
Dispatcher.DispatchEvent(this, new IPBanRemovedEvent(ipAddress));
onSuccess.Invoke(success);
}, onFailure);
}
}
}

View file

@ -1,14 +0,0 @@
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Net;
namespace SharpChat.Bans {
public interface IBanClient {
void GetBanList(Action<IEnumerable<IBanRecord>> onSuccess, Action<Exception> onFailure);
void CheckBan(IRemoteUser subject, IPAddress ipAddress, Action<IBanRecord> onSuccess, Action<Exception> onFailure);
void CreateBan(IRemoteUser subject, IRemoteUser moderator, bool perma, TimeSpan duration, string reason, Action<bool> onSuccess, Action<Exception> onFailure);
void RemoveBan(IRemoteUser subject, Action<bool> onSuccess, Action<Exception> onFailure);
void RemoveBan(IPAddress ipAddress, Action<bool> onSuccess, Action<Exception> onFailure);
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Users.Remote;
using System;
using System.Net;
namespace SharpChat.Bans {
public interface IBanRecord : IRemoteUser {
IPAddress UserIP { get; }
DateTimeOffset Expires { get; }
bool IsPermanent { get; }
}
}

View file

@ -1,159 +0,0 @@
using SharpChat.Events;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Channels {
public class Channel : IChannel, IEventHandler {
public const int ID_LENGTH = 8;
public string ChannelId { get; }
public string Name { get; private set; }
public string Topic { get; private set; }
public bool IsTemporary { get; private set; }
public int MinimumRank { get; private set; }
public bool AutoJoin { get; private set; }
public uint MaxCapacity { get; private set; }
public int Order { get; private set; }
public long OwnerId { get; private set; }
private readonly object Sync = new();
private HashSet<long> Users { get; } = new();
private Dictionary<string, long> Sessions { get; } = new();
public bool HasTopic
=> !string.IsNullOrWhiteSpace(Topic);
public string Password { get; private set; } = string.Empty;
public bool HasPassword
=> !string.IsNullOrWhiteSpace(Password);
public Channel(
string channelId,
string name,
string topic,
bool temp,
int minimumRank,
string password,
bool autoJoin,
uint maxCapacity,
long ownerId,
int order
) {
ChannelId = channelId ?? throw new ArgumentNullException(nameof(channelId));
Name = name ?? throw new ArgumentNullException(nameof(name));
Topic = topic;
IsTemporary = temp;
MinimumRank = minimumRank;
Password = password ?? string.Empty;
AutoJoin = autoJoin;
MaxCapacity = maxCapacity;
OwnerId = ownerId;
Order = order;
}
public bool VerifyPassword(string password) {
if(password == null)
throw new ArgumentNullException(nameof(password));
lock(Sync)
return !HasPassword || Password.Equals(password);
}
public bool HasUser(IUser user) {
if(user == null)
return false;
lock(Sync)
return Users.Contains(user.UserId);
}
public bool HasSession(ISession session) {
if(session == null)
return false;
lock(Sync)
return Sessions.ContainsKey(session.SessionId);
}
public void GetUserIds(Action<IEnumerable<long>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Users);
}
public void GetSessionIds(Action<IEnumerable<string>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Sessions.Keys);
}
public int CountUsers() {
lock(Sync)
return Users.Count;
}
public int CountUserSessions(IUser user) {
if(user == null)
throw new ArgumentNullException(nameof(user));
lock(Sync)
return Sessions.Values.Count(u => u == user.UserId);
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case ChannelUpdateEvent update: // Owner?
lock(Sync) {
if(update.HasName)
Name = update.Name;
if(update.HasTopic)
Topic = update.Topic;
if(update.IsTemporary.HasValue)
IsTemporary = update.IsTemporary.Value;
if(update.MinimumRank.HasValue)
MinimumRank = update.MinimumRank.Value;
if(update.HasPassword)
Password = update.Password;
if(update.AutoJoin.HasValue)
AutoJoin = update.AutoJoin.Value;
if(update.MaxCapacity.HasValue)
MaxCapacity = update.MaxCapacity.Value;
if(update.Order.HasValue)
Order = update.Order.Value;
}
break;
case ChannelUserJoinEvent cuje:
lock(Sync) {
Sessions.Add(cuje.SessionId, cuje.UserId);
Users.Add(cuje.UserId);
}
break;
case ChannelSessionJoinEvent csje:
lock(Sync)
Sessions.Add(csje.SessionId, csje.UserId);
break;
case ChannelUserLeaveEvent cule:
lock(Sync) {
Users.Remove(cule.UserId);
Queue<string> delete = new(Sessions.Where(s => s.Value == cule.UserId).Select(s => s.Key));
while(delete.TryDequeue(out string sessionId))
Sessions.Remove(sessionId);
}
break;
case ChannelSessionLeaveEvent csle:
lock(Sync)
Sessions.Remove(csle.SessionId);
break;
}
}
public bool Equals(IChannel other)
=> other != null && ChannelId.Equals(other.ChannelId);
public override string ToString()
=> $@"<Channel {ChannelId}#{Name}>";
}
}

View file

@ -1,447 +0,0 @@
using SharpChat.Configuration;
using SharpChat.Events;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Channels {
public class ChannelException : Exception { }
public class ChannelExistException : ChannelException { }
public class ChannelInvalidNameException : ChannelException { }
public class ChannelManager : IEventHandler {
private Dictionary<string, Channel> Channels { get; } = new();
private IConfig Config { get; }
private CachedValue<string[]> ChannelIds { get; }
private IEventDispatcher Dispatcher { get; }
private ChatBot Bot { get; }
private object Sync { get; } = new();
public ChannelManager(IEventDispatcher dispatcher, IConfig config, ChatBot bot) {
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Config = config ?? throw new ArgumentNullException(nameof(config));
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
ChannelIds = Config.ReadCached(@"channels", new[] { @"lounge" });
}
public void UpdateChannels() {
lock(Sync) {
string[] channelIds = ChannelIds.Value.Clone() as string[];
foreach(IChannel channel in Channels.Values) {
if(channelIds.Contains(channel.ChannelId)) {
using IConfig config = Config.ScopeTo($@"channels:{channel.ChannelId}");
string name = config.ReadValue(@"name", channel.ChannelId);
string topic = config.ReadValue(@"topic");
bool autoJoin = config.ReadValue(@"autoJoin", false);
string password = null;
int? minRank = null;
uint? maxCapacity = null;
if(!autoJoin) {
password = config.ReadValue(@"password", string.Empty);
if(string.IsNullOrEmpty(password))
password = null;
minRank = config.SafeReadValue(@"minRank", 0);
maxCapacity = config.SafeReadValue(@"maxCapacity", 0u);
}
Update(channel, name, topic, false, minRank, password, autoJoin, maxCapacity);
} else if(!channel.IsTemporary) // Not in config == temporary
Update(channel, temporary: true);
}
foreach(string channelId in channelIds) {
if(Channels.ContainsKey(channelId))
continue;
using IConfig config = Config.ScopeTo($@"channels:{channelId}");
string name = config.ReadValue(@"name", channelId);
string topic = config.ReadValue(@"topic");
bool autoJoin = config.ReadValue(@"autoJoin", false);
string password = null;
int minRank = 0;
uint maxCapacity = 0;
if(!autoJoin) {
password = config.ReadValue(@"password", string.Empty);
if(string.IsNullOrEmpty(password))
password = null;
minRank = config.SafeReadValue(@"minRank", 0);
maxCapacity = config.SafeReadValue(@"maxCapacity", 0u);
}
Create(channelId, Bot.UserId, name, topic, false, minRank, password, autoJoin, maxCapacity);
}
}
}
public void Remove(IChannel channel, IUser user = null) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
lock(Sync) {
Channel chan = null;
if(channel is Channel c && Channels.ContainsValue(c))
chan = c;
else if(Channels.TryGetValue(channel.ChannelId, out Channel c2))
chan = c2;
if(chan == null)
return; // exception?
// Remove channel from the listing
Channels.Remove(chan.ChannelId);
// Broadcast death
Dispatcher.DispatchEvent(this, new ChannelDeleteEvent(user ?? Bot, chan));
// 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.
// Could be handled by the user/session itself?
//foreach(ChatUser user in channel.GetUsers()) {
// Context.SwitchChannel(user, DefaultChannel);
//}
// Broadcast deletion of channel (deprecated)
/*foreach(IUser u in Users.OfRank(chan.MinimumRank))
u.SendPacket(new ChannelDeletePacket(chan));*/
}
}
private bool Exists(string name) {
if(name == null)
throw new ArgumentNullException(nameof(name));
lock(Sync)
return Channels.Values.Any(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
}
private void ValidateName(string name) {
if(!name.All(c => char.IsLetter(c) || char.IsNumber(c) || c == '-'))
throw new ChannelInvalidNameException();
if(Exists(name))
throw new ChannelExistException();
}
public IChannel Create(
IUser user,
string name,
string topic = null,
bool temp = true,
int minRank = 0,
string password = null,
bool autoJoin = false,
uint maxCapacity = 0
) {
if(user == null)
throw new ArgumentNullException(nameof(user));
return Create(user.UserId, name, topic, temp, minRank, password, autoJoin, maxCapacity);
}
public IChannel Create(
long ownerId,
string name,
string topic = null,
bool temp = true,
int minRank = 0,
string password = null,
bool autoJoin = false,
uint maxCapacity = 0
) => Create(RNG.NextString(Channel.ID_LENGTH), ownerId, name, topic, temp, minRank, password, autoJoin, maxCapacity);
public IChannel Create(
string channelId,
long ownerId,
string name,
string topic = null,
bool temp = true,
int minRank = 0,
string password = null,
bool autoJoin = false,
uint maxCapacity = 0,
int order = 0
) {
if(name == null)
throw new ArgumentNullException(nameof(name));
ValidateName(name);
lock(Sync) {
Channel channel = new(channelId, name, topic, temp, minRank, password, autoJoin, maxCapacity, ownerId, order);
Channels.Add(channel.ChannelId, channel);
Dispatcher.DispatchEvent(this, new ChannelCreateEvent(channel));
// Broadcast creation of channel (deprecated)
/*if(Users != null)
foreach(IUser user in Users.OfRank(channel.MinimumRank))
user.SendPacket(new ChannelCreatePacket(channel));*/
return channel;
}
}
public void Update(
IChannel channel,
string name = null,
string topic = null,
bool? temporary = null,
int? minRank = null,
string password = null,
bool? autoJoin = null,
uint? maxCapacity = null,
int? order = null
) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(!(channel is Channel c && Channels.ContainsValue(c))) {
if(Channels.TryGetValue(channel.ChannelId, out Channel c2))
channel = c2;
else
throw new ArgumentException(@"Provided channel is not registered with this manager.", nameof(channel));
}
lock(Sync) {
string prevName = channel.Name;
bool nameUpdated = !string.IsNullOrWhiteSpace(name) && name != prevName;
if(nameUpdated)
ValidateName(name);
if(topic != null && channel.Topic.Equals(topic))
topic = null;
if(temporary.HasValue && channel.IsTemporary == temporary.Value)
temporary = null;
if(minRank.HasValue && channel.MinimumRank == minRank.Value)
minRank = null;
if(password != null && channel.Password == password)
password = null;
if(autoJoin.HasValue && channel.AutoJoin == autoJoin.Value)
autoJoin = null;
if(maxCapacity.HasValue && channel.MaxCapacity == maxCapacity.Value)
maxCapacity = null;
if(order.HasValue && channel.Order == order.Value)
order = null;
Dispatcher.DispatchEvent(this, new ChannelUpdateEvent(channel, Bot, name, topic, temporary, minRank, password, autoJoin, maxCapacity, order));
// Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively
// TODO: should be moved to the usermanager probably
/*foreach(IUser user in Users.OfRank(channel.MinimumRank)) {
user.SendPacket(new ChannelUpdatePacket(prevName, channel));
if(nameUpdated)
user.ForceChannel();
}*/
}
}
public void GetChannel(Func<IChannel, bool> predicate, Action<IChannel> callback) {
if(predicate == null)
throw new ArgumentNullException(nameof(predicate));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Channels.Values.FirstOrDefault(predicate));
}
public void GetChannelById(string channelId, Action<IChannel> callback) {
if(channelId == null)
throw new ArgumentNullException(nameof(channelId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelId)) {
callback(null);
return;
}
lock(Sync)
callback(Channels.TryGetValue(channelId, out Channel channel) ? channel : null);
}
public void GetChannelByName(string name, Action<IChannel> callback) {
if(name == null)
throw new ArgumentNullException(nameof(name));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(name)) {
callback(null);
return;
}
GetChannel(c => name.Equals(c.Name, StringComparison.InvariantCultureIgnoreCase), callback);
}
public void GetChannel(IChannel channel, Action<IChannel> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync) {
if(channel is Channel c && Channels.ContainsValue(c)) {
callback(c);
return;
}
GetChannel(channel.Equals, callback);
}
}
public void GetChannels(Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync) {
IEnumerable<IChannel> channels = Channels.Values;
if(ordered)
channels = channels.OrderBy(c => c.Order);
callback(channels);
}
}
public void GetChannels(Func<IChannel, bool> predicate, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(predicate == null)
throw new ArgumentNullException(nameof(predicate));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync) {
IEnumerable<IChannel> channels = Channels.Values.Where(predicate);
if(ordered)
channels = channels.OrderBy(c => c.Order);
callback(channels);
}
}
public void GetDefaultChannels(Action<IEnumerable<IChannel>> callback, bool ordered = true) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
// it doesn't really make sense for a channel to be temporary and autojoin
// maybe reconsider this in the future if the temp channel nuking strategy has adjusted
GetChannels(c => c.AutoJoin && !c.IsTemporary, callback, ordered);
}
public void GetChannelsById(IEnumerable<string> channelIds, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(channelIds == null)
throw new ArgumentNullException(nameof(channelIds));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => channelIds.Contains(c.ChannelId), callback, ordered);
}
public void GetChannelsByName(IEnumerable<string> names, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(names == null)
throw new ArgumentNullException(nameof(names));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => names.Contains(c.Name), callback, ordered);
}
public void GetChannels(IEnumerable<IChannel> channels, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(channels == null)
throw new ArgumentNullException(nameof(channels));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c1 => channels.Any(c2 => c2.Equals(c1)), callback, ordered);
}
public void GetChannels(int minRank, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => c.MinimumRank <= minRank, callback, ordered);
}
public void GetChannels(IUser user, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => c is Channel channel && channel.HasUser(user), callback, ordered);
}
public void VerifyPassword(IChannel channel, string password, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(password == null)
throw new ArgumentNullException(nameof(password));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannel(channel, c => {
if(c is not Channel channel) {
callback(false);
return;
}
if(!channel.HasPassword) {
callback(true);
return;
}
callback(channel.VerifyPassword(password));
});
}
private void OnCreate(object sender, ChannelCreateEvent cce) {
if(sender == this)
return;
lock(Sync) {
if(Exists(cce.Name))
throw new ArgumentException(@"Channel already registered??????", nameof(cce));
Channels.Add(cce.ChannelId, new Channel(
cce.ChannelId,
cce.Name,
cce.Topic,
cce.IsTemporary,
cce.MinimumRank,
cce.Password,
cce.AutoJoin,
cce.MaxCapacity,
cce.UserId,
cce.Order
));
}
}
private void OnDelete(object sender, ChannelDeleteEvent cde) {
if(sender == this)
return;
lock(Sync)
Channels.Remove(cde.ChannelId);
}
private void OnEvent(object sender, IEvent evt) {
Channel channel;
lock(Sync)
if(!Channels.TryGetValue(evt.ChannelId, out channel))
channel = null;
channel?.HandleEvent(sender, evt);
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case ChannelCreateEvent cce:
OnCreate(sender, cce);
break;
case ChannelDeleteEvent cde:
OnDelete(sender, cde);
break;
case ChannelUpdateEvent _:
case ChannelUserJoinEvent _:
case ChannelUserLeaveEvent _:
OnEvent(sender, evt);
break;
}
}
}
}

View file

@ -1,421 +0,0 @@
using SharpChat.Events;
using SharpChat.Messages;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Channels {
public class ChannelUserRelations : IEventHandler {
private IEventDispatcher Dispatcher { get; }
private ChannelManager Channels { get; }
private UserManager Users { get; }
private SessionManager Sessions { get; }
private MessageManager Messages { get; }
public ChannelUserRelations(
IEventDispatcher dispatcher,
ChannelManager channels,
UserManager users,
SessionManager sessions,
MessageManager messages
) {
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Channels = channels ?? throw new ArgumentNullException(nameof(channels));
Users = users ?? throw new ArgumentNullException(nameof(users));
Sessions = sessions ?? throw new ArgumentNullException(nameof(sessions));
Messages = messages ?? throw new ArgumentNullException(nameof(messages));
}
public void HasUser(IChannel channel, IUser user, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(false);
return;
}
callback(channel.HasUser(user));
});
}
public void HasSession(IChannel channel, ISession session, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(session == null)
throw new ArgumentNullException(nameof(session));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(false);
return;
}
callback(channel.HasSession(session));
});
}
public void CountUsers(IChannel channel, Action<int> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(-1);
return;
}
callback(channel.CountUsers());
});
}
public void CountUserSessions(IChannel channel, IUser user, Action<int> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(-1);
return;
}
callback(channel.CountUserSessions(user));
});
}
public void CheckOverCapacity(IChannel channel, IUser user, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, channel => {
if(channel == null) {
callback(true);
return;
}
if(!channel.HasMaxCapacity() || user.UserId == channel.OwnerId) {
callback(false);
return;
}
CountUsers(channel, userCount => callback(channel == null || userCount >= channel.MaxCapacity));
});
}
public void GetUsersByChannelId(string channelId, Action<IEnumerable<ILocalUser>> callback) {
if(channelId == null)
throw new ArgumentNullException(nameof(channelId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelId)) {
callback(Enumerable.Empty<ILocalUser>());
return;
}
Channels.GetChannelById(channelId, c => GetUsersWithChannelCallback(c, callback));
}
public void GetUsersByChannelName(string channelName, Action<IEnumerable<ILocalUser>> callback) {
if(channelName == null)
throw new ArgumentNullException(nameof(channelName));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelName)) {
callback(Enumerable.Empty<ILocalUser>());
return;
}
Channels.GetChannelByName(channelName, c => GetUsersWithChannelCallback(c, callback));
}
public void GetUsers(IChannel channel, Action<IEnumerable<ILocalUser>> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => GetUsersWithChannelCallback(c, callback));
}
private void GetUsersWithChannelCallback(IChannel c, Action<IEnumerable<ILocalUser>> callback) {
if(c is not Channel channel) {
callback(Enumerable.Empty<ILocalUser>());
return;
}
channel.GetUserIds(ids => Users.GetUsers(ids, callback));
}
public void GetUsers(IEnumerable<IChannel> channels, Action<IEnumerable<ILocalUser>> callback) {
if(channels == null)
throw new ArgumentNullException(nameof(channels));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
// this is pretty disgusting
Channels.GetChannels(channels, channels => {
HashSet<long> ids = new();
foreach(IChannel c in channels) {
if(c is not Channel channel)
continue;
channel.GetUserIds(u => {
foreach(long id in u)
ids.Add(id);
});
}
Users.GetUsers(ids, callback);
});
}
// this makes me cry
public void GetUsers(IUser user, Action<IEnumerable<ILocalUser>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
HashSet<ILocalUser> all = new();
Channels.GetChannels(channels => {
foreach(IChannel channel in channels) {
GetUsers(channel, users => {
foreach(ILocalUser user in users)
all.Add(user);
});
}
});
callback(all);
}
public void GetLocalSessionsByChannelId(string channelId, Action<IEnumerable<ISession>> callback) {
if(channelId == null)
throw new ArgumentNullException(nameof(channelId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelId)) {
callback(Enumerable.Empty<ISession>());
return;
}
Channels.GetChannelById(channelId, c => GetLocalSessionsChannelCallback(c, callback));
}
public void GetLocalSessionsByChannelName(string channelName, Action<IEnumerable<ISession>> callback) {
if(channelName == null)
throw new ArgumentNullException(nameof(channelName));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelName)) {
callback(Enumerable.Empty<ISession>());
return;
}
Channels.GetChannelByName(channelName, c => GetLocalSessionsChannelCallback(c, callback));
}
public void GetLocalSessions(IChannel channel, Action<IEnumerable<ISession>> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => GetLocalSessionsChannelCallback(c, callback));
}
private void GetLocalSessionsChannelCallback(IChannel c, Action<IEnumerable<ISession>> callback) {
if(c is not Channel channel) {
callback(Enumerable.Empty<ISession>());
return;
}
channel.GetSessionIds(ids => Sessions.GetLocalSessions(ids, callback));
}
public void GetLocalSessions(IUser user, Action<IEnumerable<ISession>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(user, channels => GetLocalSessionsUserCallback(channels, callback));
}
public void GetLocalSessionsByUserId(long userId, Action<IUser, IEnumerable<ISession>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(userId < 1) {
callback(null, Enumerable.Empty<ISession>());
return;
}
GetChannelsByUserId(userId, (user, channels) => GetLocalSessionsUserCallback(channels, sessions => callback(user, sessions)));
}
private void GetLocalSessionsUserCallback(IEnumerable<IChannel> channels, Action<IEnumerable<ISession>> callback) {
if(!channels.Any()) {
callback(Enumerable.Empty<ISession>());
return;
}
Channels.GetChannels(channels, channels => {
HashSet<string> sessionIds = new();
foreach(IChannel c in channels) {
if(c is not Channel channel)
continue;
channel.GetSessionIds(ids => {
foreach(string id in ids)
sessionIds.Add(id);
});
}
Sessions.GetLocalSessions(sessionIds, callback);
});
}
public void GetChannelsByUserId(long userId, Action<IUser, IEnumerable<IChannel>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(userId < 1) {
callback(null, Enumerable.Empty<IChannel>());
return;
}
Users.GetUser(userId, u => GetChannelsUserCallback(u, channels => callback(u, channels)));
}
public void GetChannels(IUser user, Action<IEnumerable<IChannel>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Users.GetUser(user, u => GetChannelsUserCallback(u, callback));
}
private void GetChannelsUserCallback(IUser u, Action<IEnumerable<IChannel>> callback) {
if(u is not User user) {
callback(Enumerable.Empty<IChannel>());
return;
}
user.GetChannels(c => Channels.GetChannelsByName(c, callback));
}
public void JoinChannel(IChannel channel, ISession session) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(session == null)
throw new ArgumentNullException(nameof(session));
HasSession(channel, session, hasSession => {
if(hasSession)
return;
// SessionJoin and UserJoin should be combined
HasUser(channel, session.User, HasUser => {
Dispatcher.DispatchEvent(
this,
HasUser
? new ChannelSessionJoinEvent(channel, session)
: new ChannelUserJoinEvent(channel, session)
);
});
});
}
public void LeaveChannel(IChannel channel, IUser user, UserDisconnectReason reason = UserDisconnectReason.Unknown) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
HasUser(channel, user, hasUser => {
if(hasUser)
Dispatcher.DispatchEvent(this, new ChannelUserLeaveEvent(user, channel, reason));
});
}
public void LeaveChannel(IChannel channel, ISession session) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(session == null)
throw new ArgumentNullException(nameof(session));
HasSession(channel, session, hasSession => {
// UserLeave and SessionLeave should be combined
CountUserSessions(channel, session.User, sessionCount => {
Dispatcher.DispatchEvent(
this,
sessionCount <= 1
? new ChannelUserLeaveEvent(session.User, channel, UserDisconnectReason.Leave)
: new ChannelSessionLeaveEvent(channel, session)
);
});
});
}
public void LeaveChannels(ISession session) {
if(session == null)
throw new ArgumentNullException(nameof(session));
Channels.GetChannels(channels => {
foreach(IChannel channel in channels)
LeaveChannel(channel, session);
});
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case UserUpdateEvent uue: // fetch up to date user info
GetChannelsByUserId(evt.UserId, (user, channels) => GetUsers(channels, users => {
foreach(ILocalUser user in users)
GetLocalSessions(user, sessions => {
foreach(ISession session in sessions)
session.HandleEvent(sender, new UserUpdateEvent(user, uue));
});
}));
break;
case ChannelUserJoinEvent cje:
// THIS DOES NOT DO WHAT YOU WANT IT TO DO
// I THINK
// it really doesn't, figure out how to leave channels when MCHAN isn't active for the session
//if((Sessions.GetCapabilities(cje.User) & ClientCapability.MCHAN) == 0)
// LeaveChannel(cje.Channel, cje.User, UserDisconnectReason.Leave);
break;
case ChannelUserLeaveEvent cle: // Should ownership just be passed on to another user instead of Destruction?
Channels.GetChannelById(evt.ChannelId, channel => {
if(channel.IsTemporary && evt.UserId == channel.OwnerId)
Channels.Remove(channel);
});
break;
case SessionDestroyEvent sde:
Users.GetUser(sde.UserId, user => {
if(user == null)
return;
Sessions.GetSessionCount(user, sessionCount => {
if(sessionCount < 1)
Users.Disconnect(user, UserDisconnectReason.TimeOut);
});
});
break;
}
}
}
}

View file

@ -1,18 +0,0 @@
using System;
namespace SharpChat.Channels {
public interface IChannel : IEquatable<IChannel> {
string ChannelId { get; }
string Name { get; }
string Topic { get; }
bool IsTemporary { get; }
int MinimumRank { get; }
bool AutoJoin { get; }
uint MaxCapacity { get; }
int Order { get; }
long OwnerId { get; }
string Password { get; }
bool HasPassword { get; }
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Users;
namespace SharpChat.Channels {
public static class IChannelExtensions {
public static bool HasMaxCapacity(this IChannel channel)
=> channel.MaxCapacity != 0;
public static bool IsOwner(this IChannel channel, IUser user)
=> channel != null && user != null && channel.OwnerId == user.UserId;
}
}

View file

@ -1,29 +0,0 @@
using System;
namespace SharpChat {
public readonly struct Colour : IEquatable<Colour?> {
public const int INHERIT = 0x40000000;
public int Raw { get; }
public Colour(int argb) {
Raw = argb;
}
public static implicit operator Colour(int argb) => new(argb);
public bool Equals(Colour? other)
=> other.HasValue && other.Value.Raw == Raw;
public bool Inherit => (Raw & INHERIT) > 0;
public int Red => (Raw >> 16) & 0xFF;
public int Green => (Raw >> 8) & 0xFF;
public int Blue => Raw & 0xFF;
public override string ToString() {
if (Inherit)
return @"inherit";
return string.Format(@"#{0:X6}", Raw);
}
}
}

View file

@ -1,45 +0,0 @@
using System;
namespace SharpChat.Configuration {
public class CachedValue<T> {
private IConfig Config { get; }
private string Name { get; }
private TimeSpan Lifetime { get; }
private T Fallback { get; }
private object Sync { get; } = new();
private object CurrentValue { get; set; }
private DateTimeOffset LastRead { get; set; }
public T Value {
get {
lock(Sync) {
DateTimeOffset now = DateTimeOffset.Now;
if((now - LastRead) >= Lifetime) {
LastRead = now;
CurrentValue = Config.ReadValue(Name, Fallback);
Logger.Debug($@"Read {Name} ({CurrentValue})");
}
}
return (T)CurrentValue;
}
}
public static implicit operator T(CachedValue<T> val) => val.Value;
public CachedValue(IConfig config, string name, TimeSpan lifetime, T fallback) {
Config = config ?? throw new ArgumentNullException(nameof(config));
Name = name ?? throw new ArgumentNullException(nameof(name));
Lifetime = lifetime;
Fallback = fallback;
if(string.IsNullOrWhiteSpace(name))
throw new ArgumentException(@"Name cannot be empty.", nameof(name));
}
public void Refresh() {
lock(Sync) {
LastRead = DateTimeOffset.MinValue;
}
}
}
}

View file

@ -1,16 +0,0 @@
using System;
namespace SharpChat.Configuration {
public abstract class ConfigException : Exception {
public ConfigException(string message) : base(message) { }
public ConfigException(string message, Exception ex) : base(message, ex) { }
}
public class ConfigLockException : ConfigException {
public ConfigLockException() : base(@"Unable to acquire lock for reading configuration.") { }
}
public class ConfigTypeException : ConfigException {
public ConfigTypeException(Exception ex) : base(@"Given type does not match the value in the configuration.", ex) { }
}
}

View file

@ -1,31 +0,0 @@
using System;
namespace SharpChat.Configuration {
public interface IConfig : IDisposable {
/// <summary>
/// Creates a proxy object that forces all names to start with the given prefix.
/// </summary>
IConfig ScopeTo(string prefix);
/// <summary>
/// Reads a raw (string) value from the config.
/// </summary>
string ReadValue(string name, string fallback = null);
/// <summary>
/// Reads and casts value from the config.
/// </summary>
/// <exception cref="ConfigTypeException">Type conversion failed.</exception>
T ReadValue<T>(string name, T fallback = default);
/// <summary>
/// Reads and casts a value from the config. Returns fallback when type conversion fails.
/// </summary>
T SafeReadValue<T>(string name, T fallback);
/// <summary>
/// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values.
/// </summary>
CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null);
}
}

View file

@ -1,45 +0,0 @@
using System;
namespace SharpChat.Configuration {
public class ScopedConfig : IConfig {
private IConfig Config { get; }
private string Prefix { get; }
public ScopedConfig(IConfig config, string prefix) {
Config = config ?? throw new ArgumentNullException(nameof(config));
Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix));
if(string.IsNullOrWhiteSpace(prefix))
throw new ArgumentException(@"Prefix must exist.", nameof(prefix));
if(Prefix[^1] != ':')
Prefix += ':';
}
private string GetName(string name) {
return Prefix + name;
}
public string ReadValue(string name, string fallback = null) {
return Config.ReadValue(GetName(name), fallback);
}
public T ReadValue<T>(string name, T fallback = default) {
return Config.ReadValue(GetName(name), fallback);
}
public T SafeReadValue<T>(string name, T fallback) {
return Config.SafeReadValue(GetName(name), fallback);
}
public IConfig ScopeTo(string prefix) {
return Config.ScopeTo(GetName(prefix));
}
public CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null) {
return Config.ReadCached(GetName(name), fallback, lifetime);
}
public void Dispose() {
GC.SuppressFinalize(this);
}
}
}

View file

@ -1,112 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
namespace SharpChat.Configuration {
public class StreamConfig : IConfig {
private Stream Stream { get; }
private StreamReader StreamReader { get; }
private Mutex Lock { get; }
private const int LOCK_TIMEOUT = 10000;
private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15);
public StreamConfig(string fileName)
: this(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) {}
public StreamConfig(Stream stream) {
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
if(!Stream.CanRead)
throw new ArgumentException(@"Provided stream must be readable.", nameof(stream));
if(!Stream.CanSeek)
throw new ArgumentException(@"Provided stream must be seekable.", nameof(stream));
StreamReader = new StreamReader(stream, new UTF8Encoding(false), false);
Lock = new Mutex();
}
public string ReadValue(string name, string fallback = null) {
if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong
throw new ConfigLockException();
try {
Stream.Seek(0, SeekOrigin.Begin);
string line;
while((line = StreamReader.ReadLine()) != null) {
if(string.IsNullOrWhiteSpace(line))
continue;
line = line.TrimStart();
if(line.StartsWith(@";") || line.StartsWith(@"#"))
continue;
string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
if(parts.Length < 2 || !string.Equals(parts[0], name))
continue;
return parts[1];
}
} finally {
Lock.ReleaseMutex();
}
return fallback;
}
public T ReadValue<T>(string name, T fallback = default) {
object value = ReadValue(name);
if(value == null)
return fallback;
Type type = typeof(T);
if(value is string strVal) {
if(type == typeof(bool))
value = !string.Equals(strVal, @"0", StringComparison.InvariantCultureIgnoreCase)
&& !string.Equals(strVal, @"false", StringComparison.InvariantCultureIgnoreCase);
else if(type == typeof(string[]))
value = strVal.Split(' ');
}
try {
return (T)Convert.ChangeType(value, type);
} catch(InvalidCastException ex) {
throw new ConfigTypeException(ex);
}
}
public T SafeReadValue<T>(string name, T fallback) {
try {
return ReadValue(name, fallback);
} catch(ConfigTypeException) {
return fallback;
}
}
public IConfig ScopeTo(string prefix) {
return new ScopedConfig(this, prefix);
}
public CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null) {
return new CachedValue<T>(this, name, lifetime ?? CACHE_LIFETIME, fallback);
}
private bool IsDisposed;
~StreamConfig()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
StreamReader.Dispose();
Stream.Dispose();
Lock.Dispose();
}
}
}

View file

@ -1,145 +0,0 @@
using SharpChat.Bans;
using SharpChat.Channels;
using SharpChat.Configuration;
using SharpChat.Database;
using SharpChat.DataProvider;
using SharpChat.Events;
using SharpChat.Messages;
using SharpChat.Messages.Storage;
using SharpChat.RateLimiting;
using SharpChat.Sessions;
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace SharpChat {
public class Context : IDisposable {
public const int ID_LENGTH = 8;
public string ServerId { get; }
public EventDispatcher Events { get; }
public SessionManager Sessions { get; }
public UserManager Users { get; }
public ChannelManager Channels { get; }
public ChannelUserRelations ChannelUsers { get; }
public MessageManager Messages { get; }
public BanManager Bans { get; }
public IDataProvider DataProvider { get; }
public RateLimitManager RateLimiting { get; }
public WelcomeMessage WelcomeMessage { get; }
public ChatBot Bot { get; } = new();
private Timer BumpTimer { get; }
public DateTimeOffset Created { get; }
public Context(IConfig config, IDatabaseBackend databaseBackend, IDataProvider dataProvider) {
if(config == null)
throw new ArgumentNullException(nameof(config));
ServerId = RNG.NextString(ID_LENGTH); // maybe read this from the cfg instead
Created = DateTimeOffset.Now; // read this from config definitely
DatabaseWrapper db = new(databaseBackend ?? throw new ArgumentNullException(nameof(databaseBackend)));
IMessageStorage msgStore = db.IsNullBackend
? new MemoryMessageStorage()
: new ADOMessageStorage(db);
Events = new EventDispatcher();
DataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider));
Users = new UserManager(Events);
Sessions = new SessionManager(Events, Users, config.ScopeTo(@"sessions"), ServerId);
Messages = new MessageManager(Events, msgStore, config.ScopeTo(@"messages"));
Channels = new ChannelManager(Events, config, Bot);
ChannelUsers = new ChannelUserRelations(Events, Channels, Users, Sessions, Messages);
Bans = new BanManager(Users, DataProvider.BanClient, DataProvider.UserClient, Events);
RateLimiting = new RateLimitManager(config.ScopeTo(@"rateLimit"));
WelcomeMessage = new WelcomeMessage(config.ScopeTo(@"welcome"));
Events.AddEventHandler(Sessions);
Events.ProtectEventHandler(Sessions);
Events.AddEventHandler(Users);
Events.ProtectEventHandler(Users);
Events.AddEventHandler(Channels);
Events.ProtectEventHandler(Channels);
Events.AddEventHandler(ChannelUsers);
Events.ProtectEventHandler(ChannelUsers);
Events.AddEventHandler(Messages);
Events.ProtectEventHandler(Messages);
Events.StartProcessing();
Channels.UpdateChannels();
// Should probably not rely on Timers in the future
BumpTimer = new Timer(e => {
Logger.Write(@"Nuking dead sessions and bumping remote online status...");
Sessions.CheckTimeOut();
Sessions.GetActiveLocalSessions(sessions => {
Dictionary<IUser, List<ISession>> data = new();
foreach(ISession session in sessions) {
if(!data.ContainsKey(session.User))
data.Add(session.User, new());
data[session.User].Add(session);
}
DataProvider.UserClient.BumpUsers(
data.Select(kvp => new UserBumpInfo(kvp.Key, kvp.Value)),
() => Logger.Debug(@"Successfully bumped remote online status!"),
ex => { Logger.Write(@"Failed to bump remote online status."); Logger.Debug(ex); }
);
});
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
}
public void BroadcastMessage(string text) {
Events.DispatchEvent(this, new BroadcastMessageEvent(Bot, text));
}
[Obsolete(@"Use ChannelUsers.JoinChannel")]
public void JoinChannel(IUser user, IChannel channel) {
// handle in channelusers
//channel.SendPacket(new UserChannelJoinPacket(user));
// send after join packet for v1
//user.SendPacket(new ContextClearPacket(channel, ContextClearMode.MessagesUsers));
// send after join
//ChannelUsers.GetUsers(channel, u => user.SendPacket(new ContextUsersPacket(u.Except(new[] { user }).OrderByDescending(u => u.Rank))));
// send after join, maybe add a capability that makes this implicit?
/*Messages.GetMessages(channel, m => {
foreach(IMessage msg in m)
user.SendPacket(new ContextMessagePacket(msg));
});*/
// should happen implicitly for v1 clients
//user.ForceChannel(channel);
}
private bool IsDisposed;
~Context()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if (IsDisposed)
return;
IsDisposed = true;
BumpTimer.Dispose();
Events.FinishProcessing();
}
}
}

View file

@ -1,8 +0,0 @@
using SharpChat.Reflection;
namespace SharpChat.DataProvider {
public class DataProviderAttribute : ObjectConstructorAttribute {
public DataProviderAttribute(string name) : base(name) {
}
}
}

View file

@ -1,9 +0,0 @@
using SharpChat.Bans;
using SharpChat.Users.Remote;
namespace SharpChat.DataProvider {
public interface IDataProvider {
IBanClient BanClient { get; }
IRemoteUserClient UserClient { get; }
}
}

View file

@ -1,30 +0,0 @@
using SharpChat.Bans;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
namespace SharpChat.DataProvider.Null {
public class NullBanClient : IBanClient {
public void CheckBan(IRemoteUser subject, IPAddress ipAddress, Action<IBanRecord> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
public void CreateBan(IRemoteUser subject, IRemoteUser moderator, bool perma, TimeSpan duration, string reason, Action<bool> onSuccess, Action<Exception> onFailure) {
onSuccess(true);
}
public void GetBanList(Action<IEnumerable<IBanRecord>> onSuccess, Action<Exception> onFailure) {
onSuccess(Enumerable.Empty<IBanRecord>());
}
public void RemoveBan(IRemoteUser subject, Action<bool> onSuccess, Action<Exception> onFailure) {
onSuccess(false);
}
public void RemoveBan(IPAddress ipAddress, Action<bool> onSuccess, Action<Exception> onFailure) {
onSuccess(false);
}
}
}

View file

@ -1,15 +0,0 @@
using SharpChat.Bans;
using SharpChat.Users.Remote;
namespace SharpChat.DataProvider.Null {
[DataProvider(@"null")]
public class NullDataProvider : IDataProvider {
public IBanClient BanClient { get; }
public IRemoteUserClient UserClient { get; }
public NullDataProvider() {
BanClient = new NullBanClient();
UserClient = new NullUserClient();
}
}
}

View file

@ -1,28 +0,0 @@
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
namespace SharpChat.DataProvider.Null {
public class NullUserAuthResponse : IUserAuthResponse {
public long UserId { get; }
public string UserName { get; }
public int Rank { get; }
public Colour Colour { get; }
public UserPermissions Permissions { get; }
public DateTimeOffset SilencedUntil => DateTimeOffset.MinValue;
public NullUserAuthResponse(UserAuthRequest uar) {
UserId = uar.UserId;
UserName = $@"Misaka-{uar.UserId}";
Rank = (int)(uar.UserId % 10);
Random rng = new((int)uar.UserId);
Colour = new(rng.Next());
Permissions = (UserPermissions)rng.Next();
}
public bool Equals(IUser other)
=> other is NullUserAuthResponse && other.UserId == UserId;
public bool Equals(IRemoteUser other)
=> other is NullUserAuthResponse && other.UserId == UserId;
}
}

View file

@ -1,33 +0,0 @@
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
namespace SharpChat.DataProvider.Null {
public class NullUserClient : IRemoteUserClient {
public void AuthenticateUser(UserAuthRequest request, Action<IUserAuthResponse> onSuccess, Action<Exception> onFailure) {
if(request.Token.StartsWith(@"FAIL:")) {
onFailure(new UserAuthFailedException(request.Token[5..]));
return;
}
onSuccess(new NullUserAuthResponse(request));
}
public void BumpUsers(IEnumerable<UserBumpInfo> users, Action onSuccess, Action<Exception> onFailure) {
onSuccess();
}
public void ResolveUser(long userId, Action<IRemoteUser> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
public void ResolveUser(string userName, Action<IRemoteUser> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
public void ResolveUser(IUser localUser, Action<IRemoteUser> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
}
}

View file

@ -1,81 +0,0 @@
using System;
using System.Data.Common;
namespace SharpChat.Database {
public class ADODatabaseReader : IDatabaseReader {
private DbDataReader Reader { get; }
public ADODatabaseReader(DbDataReader reader) {
Reader = reader;
}
public bool Next()
=> Reader.Read();
public string GetName(int ordinal)
=> Reader.GetName(ordinal);
public int GetOrdinal(string name)
=> Reader.GetOrdinal(name);
public bool IsNull(int ordinal)
=> Reader.IsDBNull(ordinal);
public bool IsNull(string name)
=> Reader.IsDBNull(GetOrdinal(name));
public object GetValue(int ordinal)
=> Reader.GetValue(ordinal);
public object GetValue(string name)
=> Reader.GetValue(GetOrdinal(name));
public string ReadString(int ordinal)
=> Reader.GetString(ordinal);
public string ReadString(string name)
=> Reader.GetString(GetOrdinal(name));
public byte ReadU8(int ordinal)
=> Reader.GetByte(ordinal);
public byte ReadU8(string name)
=> Reader.GetByte(GetOrdinal(name));
public short ReadI16(int ordinal)
=> Reader.GetInt16(ordinal);
public short ReadI16(string name)
=> Reader.GetInt16(GetOrdinal(name));
public int ReadI32(int ordinal)
=> Reader.GetInt32(ordinal);
public int ReadI32(string name)
=> Reader.GetInt32(GetOrdinal(name));
public long ReadI64(int ordinal)
=> Reader.GetInt64(ordinal);
public long ReadI64(string name)
=> Reader.GetInt64(GetOrdinal(name));
public float ReadF32(int ordinal)
=> Reader.GetFloat(ordinal);
public float ReadF32(string name)
=> Reader.GetFloat(GetOrdinal(name));
public double ReadF64(int ordinal)
=> Reader.GetDouble(ordinal);
public double ReadF64(string name)
=> Reader.GetDouble(GetOrdinal(name));
private bool IsDisposed;
~ADODatabaseReader()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
if(Reader is IDisposable disposable)
disposable.Dispose();
}
}
}

View file

@ -1,8 +0,0 @@
using SharpChat.Reflection;
namespace SharpChat.Database {
public class DatabaseBackendAttribute : ObjectConstructorAttribute {
public DatabaseBackendAttribute(string name) : base(name) {
}
}
}

View file

@ -1,7 +0,0 @@
using System;
namespace SharpChat.Database {
public class DatabaseException : Exception {}
public class InvalidParameterClassTypeException : DatabaseException { }
}

View file

@ -1,14 +0,0 @@
namespace SharpChat.Database {
public enum DatabaseType {
AsciiString,
UnicodeString,
Int8,
Int16,
Int32,
Int64,
UInt8,
UInt16,
UInt32,
UInt64,
}
}

View file

@ -1,109 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Database {
public class DatabaseWrapper {
private IDatabaseBackend Backend { get; }
public bool IsNullBackend
=> Backend is Null.NullDatabaseBackend;
public DatabaseWrapper(IDatabaseBackend backend) {
Backend = backend ?? throw new ArgumentNullException(nameof(backend));
}
public IDatabaseParameter CreateParam(string name, object value)
=> Backend.CreateParameter(name, value);
public string TimestampType
=> Backend.TimestampType;
public string TextType
=> Backend.TextType;
public string BlobType
=> Backend.BlobType;
public string VarCharType(int size)
=> Backend.VarCharType(size);
public string VarBinaryType(int size)
=> Backend.VarBinaryType(size);
public string BigIntType(int length)
=> Backend.BigIntType(length);
public string BigUIntType(int length)
=> Backend.BigUIntType(length);
public string IntType(int length)
=> Backend.IntType(length);
public string UIntType(int length)
=> Backend.UIntType(length);
public string TinyIntType(int length)
=> Backend.TinyIntType(length);
public string TinyUIntType(int length)
=> Backend.TinyUIntType(length);
public string ToUnixTime(string param)
=> Backend.ToUnixTime(param);
public string FromUnixTime(string param)
=> Backend.FromUnixTime(param);
public string DateTimeNow()
=> Backend.DateTimeNow();
public string Concat(params string[] args)
=> Backend.Concat(args);
public string ToLower(string param)
=> Backend.ToLower(param);
public bool SupportsJson
=> Backend.SupportsJson;
public string JsonValue(string field, string path)
=> Backend.JsonValue(field, path);
public bool SupportsAlterTableCollate
=> Backend.SupportsAlterTableCollate;
public string AsciiCollation
=> Backend.AsciiCollation;
public string UnicodeCollation
=> Backend.UnicodeCollation;
public void RunCommand(object query, int timeout, Action<IDatabaseCommand> action, params IDatabaseParameter[] @params) {
#if LOG_SQL
Logger.Debug(query);
#endif
using IDatabaseConnection conn = Backend.CreateConnection();
using IDatabaseCommand comm = conn.CreateCommand(query);
comm.CommandTimeout = timeout;
if(@params.Any()) {
comm.AddParameters(@params);
comm.Prepare();
}
action.Invoke(comm);
}
public void RunCommand(object query, Action<IDatabaseCommand> action, params IDatabaseParameter[] @params)
=> RunCommand(query, 30, action, @params);
public int RunCommand(object query, params IDatabaseParameter[] @params) {
int affected = 0;
RunCommand(query, comm => affected = comm.Execute(), @params);
return affected;
}
public int RunCommand(object query, int timeout, params IDatabaseParameter[] @params) {
int affected = 0;
RunCommand(query, timeout, comm => affected = comm.Execute(), @params);
return affected;
}
public object RunQueryValue(object query, params IDatabaseParameter[] @params) {
object value = null;
RunCommand(query, comm => value = comm.ExecuteScalar(), @params);
return value;
}
public void RunQuery(object query, Action<IDatabaseReader> action, params IDatabaseParameter[] @params) {
RunCommand(query, comm => {
using IDatabaseReader reader = comm.ExecuteReader();
action.Invoke(reader);
}, @params);
}
}
}

View file

@ -1,37 +0,0 @@
using System.Collections.Generic;
namespace SharpChat.Database {
public interface IDatabaseBackend {
IDatabaseConnection CreateConnection();
IDatabaseParameter CreateParameter(string name, object value);
IDatabaseParameter CreateParameter(string name, DatabaseType type);
string TimestampType { get; }
string TextType { get; }
string BlobType { get; }
string VarCharType(int length);
string VarBinaryType(int length);
string BigIntType(int length);
string BigUIntType(int length);
string IntType(int length);
string UIntType(int length);
string TinyIntType(int length);
string TinyUIntType(int length);
string FromUnixTime(string param);
string ToUnixTime(string param);
string DateTimeNow();
string Concat(params string[] args);
string ToLower(string param);
bool SupportsJson { get; }
string JsonValue(string field, string path);
bool SupportsAlterTableCollate { get; }
string AsciiCollation { get; }
string UnicodeCollation { get; }
}
}

View file

@ -1,21 +0,0 @@
using System;
namespace SharpChat.Database {
public interface IDatabaseCommand : IDisposable {
IDatabaseConnection Connection { get; }
string CommandString { get; }
int CommandTimeout { get; set; }
IDatabaseParameter AddParameter(string name, object value);
IDatabaseParameter AddParameter(string name, DatabaseType type);
IDatabaseParameter AddParameter(IDatabaseParameter param);
void AddParameters(IDatabaseParameter[] @params);
void ClearParameters();
void Prepare();
int Execute();
IDatabaseReader ExecuteReader();
object ExecuteScalar();
}
}

View file

@ -1,7 +0,0 @@
using System;
namespace SharpChat.Database {
public interface IDatabaseConnection : IDisposable {
IDatabaseCommand CreateCommand(object query);
}
}

View file

@ -1,6 +0,0 @@
namespace SharpChat.Database {
public interface IDatabaseParameter {
string Name { get; }
object Value { get; set; }
}
}

View file

@ -1,37 +0,0 @@
using System;
namespace SharpChat.Database {
public interface IDatabaseReader : IDisposable {
bool Next();
object GetValue(int ordinal);
object GetValue(string name);
bool IsNull(int ordinal);
bool IsNull(string name);
string GetName(int ordinal);
int GetOrdinal(string name);
string ReadString(int ordinal);
string ReadString(string name);
byte ReadU8(int ordinal);
byte ReadU8(string name);
short ReadI16(int ordinal);
short ReadI16(string name);
int ReadI32(int ordinal);
int ReadI32(string name);
long ReadI64(int ordinal);
long ReadI64(string name);
float ReadF32(int ordinal);
float ReadF32(string name);
double ReadF64(int ordinal);
double ReadF64(string name);
}
}

View file

@ -1,63 +0,0 @@
using SharpChat.Configuration;
using System.Collections.Generic;
namespace SharpChat.Database.Null {
[DatabaseBackend(@"null")]
public class NullDatabaseBackend : IDatabaseBackend {
public NullDatabaseBackend(IConfig _ = null) { }
public IDatabaseConnection CreateConnection()
=> new NullDatabaseConnection();
public IDatabaseParameter CreateParameter(string name, object value)
=> new NullDatabaseParameter();
public IDatabaseParameter CreateParameter(string name, DatabaseType type)
=> new NullDatabaseParameter();
public string TimestampType
=> string.Empty;
public string TextType
=> string.Empty;
public string BlobType
=> string.Empty;
public string VarCharType(int size)
=> string.Empty;
public string VarBinaryType(int size)
=> string.Empty;
public string BigIntType(int length)
=> string.Empty;
public string BigUIntType(int length)
=> string.Empty;
public string IntType(int length)
=> string.Empty;
public string UIntType(int length)
=> string.Empty;
public string TinyIntType(int length)
=> string.Empty;
public string TinyUIntType(int length)
=> string.Empty;
public string FromUnixTime(string param)
=> string.Empty;
public string ToUnixTime(string param)
=> string.Empty;
public string DateTimeNow()
=> string.Empty;
public string Concat(params string[] args)
=> string.Empty;
public string ToLower(string param)
=> string.Empty;
public bool SupportsJson => false;
public string JsonValue(string field, string path)
=> string.Empty;
public bool SupportsAlterTableCollate => true;
public string AsciiCollation => string.Empty;
public string UnicodeCollation => string.Empty;
}
}

View file

@ -1,47 +0,0 @@
using System;
namespace SharpChat.Database.Null {
public class NullDatabaseCommand : IDatabaseCommand {
public IDatabaseConnection Connection { get; }
public string CommandString => string.Empty;
public int CommandTimeout { get => -1; set { } }
public NullDatabaseCommand(NullDatabaseConnection conn) {
Connection = conn ?? throw new ArgumentNullException(nameof(conn));
}
public IDatabaseParameter AddParameter(string name, object value)
=> new NullDatabaseParameter();
public IDatabaseParameter AddParameter(string name, DatabaseType type)
=> new NullDatabaseParameter();
public IDatabaseParameter AddParameter(IDatabaseParameter param) {
if(param is not NullDatabaseParameter)
throw new InvalidParameterClassTypeException();
return param;
}
public void AddParameters(IDatabaseParameter[] @params) {}
public void ClearParameters() {}
public void Dispose() {
GC.SuppressFinalize(this);
}
public int Execute() {
return 0;
}
public IDatabaseReader ExecuteReader() {
return new NullDatabaseReader();
}
public object ExecuteScalar() {
return null;
}
public void Prepare() {}
}
}

View file

@ -1,13 +0,0 @@
using System;
namespace SharpChat.Database.Null {
public class NullDatabaseConnection : IDatabaseConnection {
public IDatabaseCommand CreateCommand(object query) {
return new NullDatabaseCommand(this);
}
public void Dispose() {
GC.SuppressFinalize(this);
}
}
}

View file

@ -1,6 +0,0 @@
namespace SharpChat.Database.Null {
public class NullDatabaseParameter : IDatabaseParameter {
public string Name => string.Empty;
public object Value { get => null; set { } }
}
}

View file

@ -1,92 +0,0 @@
using System;
namespace SharpChat.Database.Null {
public class NullDatabaseReader : IDatabaseReader {
public void Dispose() {
GC.SuppressFinalize(this);
}
public string GetName(int ordinal) {
return string.Empty;
}
public int GetOrdinal(string name) {
return 0;
}
public object GetValue(int ordinal) {
return null;
}
public object GetValue(string name) {
return null;
}
public bool IsNull(int ordinal) {
return true;
}
public bool IsNull(string name) {
return true;
}
public bool Next() {
return false;
}
public float ReadF32(int ordinal) {
return 0f;
}
public float ReadF32(string name) {
return 0f;
}
public double ReadF64(int ordinal) {
return 0d;
}
public double ReadF64(string name) {
return 0d;
}
public short ReadI16(int ordinal) {
return 0;
}
public short ReadI16(string name) {
return 0;
}
public int ReadI32(int ordinal) {
return 0;
}
public int ReadI32(string name) {
return 0;
}
public long ReadI64(int ordinal) {
return 0;
}
public long ReadI64(string name) {
return 0;
}
public string ReadString(int ordinal) {
return string.Empty;
}
public string ReadString(string name) {
return string.Empty;
}
public byte ReadU8(int ordinal) {
return 0;
}
public byte ReadU8(string name) {
return 0;
}
}
}

View file

@ -1,15 +0,0 @@
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class BroadcastMessageEvent : Event {
public const string TYPE = @"broadcast:message";
public string Text { get; }
public BroadcastMessageEvent(ChatBot chatBot, string text) : base(chatBot) {
Text = text ?? throw new ArgumentNullException(nameof(text));
}
}
}

View file

@ -1,28 +0,0 @@
using SharpChat.Channels;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelCreateEvent : Event {
public const string TYPE = @"channel:create";
public string Name { get; }
public string Topic { get; }
public bool IsTemporary { get; }
public int MinimumRank { get; }
public string Password { get; }
public bool AutoJoin { get; }
public uint MaxCapacity { get; }
public int Order { get; }
public ChannelCreateEvent(IChannel channel) : base(channel) {
Name = channel.Name;
Topic = channel.Topic;
IsTemporary = channel.IsTemporary;
MinimumRank = channel.MinimumRank;
Password = channel.Password;
AutoJoin = channel.AutoJoin;
MaxCapacity = channel.MaxCapacity;
Order = channel.Order;
}
}
}

View file

@ -1,13 +0,0 @@
using SharpChat.Channels;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelDeleteEvent : Event {
public const string TYPE = @"channel:delete";
public ChannelDeleteEvent(IChannel channel) : base(channel) { }
public ChannelDeleteEvent(IUser user, IChannel channel) : base(user, channel) { }
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Channels;
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelSessionJoinEvent : Event {
public const string TYPE = @"channel:session:join";
public ChannelSessionJoinEvent(IChannel channel, ISession session) : base(channel, session) { }
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Channels;
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelSessionLeaveEvent : Event {
public const string TYPE = @"channel:session:leave";
public ChannelSessionLeaveEvent(IChannel channel, ISession session) : base(channel, session) { }
}
}

View file

@ -1,47 +0,0 @@
using SharpChat.Channels;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelUpdateEvent : Event {
public const string TYPE = @"channel:update";
public string PreviousName { get; }
public string Name { get; }
public string Topic { get; }
public bool? IsTemporary { get; }
public int? MinimumRank { get; }
public string Password { get; }
public bool? AutoJoin { get; }
public uint? MaxCapacity { get; }
public int? Order { get; }
public bool HasName => Name != null;
public bool HasTopic => Topic != null;
public bool HasPassword => Password != null;
public ChannelUpdateEvent(
IChannel channel,
IUser owner,
string name,
string topic,
bool? temp,
int? minRank,
string password,
bool? autoJoin,
uint? maxCapacity,
int? order
) : base(owner, channel ?? throw new ArgumentNullException(nameof(channel))) {
PreviousName = channel.Name;
Name = name;
Topic = topic;
IsTemporary = temp;
MinimumRank = minRank;
Password = password;
AutoJoin = autoJoin;
MaxCapacity = maxCapacity;
Order = order;
}
}
}

View file

@ -1,14 +0,0 @@
using SharpChat.Channels;
using SharpChat.Sessions;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelUserJoinEvent : Event {
public const string TYPE = @"channel:user:join";
public ChannelUserJoinEvent(IUser user, IChannel channel) : base(user, channel) { }
public ChannelUserJoinEvent(IChannel channel, ISession session) : base(channel, session) { }
}
}

View file

@ -1,17 +0,0 @@
using SharpChat.Channels;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelUserLeaveEvent : Event {
public const string TYPE = @"channel:user:leave";
public UserDisconnectReason Reason { get; }
public ChannelUserLeaveEvent(IUser user, IChannel channel, UserDisconnectReason reason)
: base(user, channel) {
Reason = reason;
}
}
}

View file

@ -1,83 +0,0 @@
using SharpChat.Channels;
using SharpChat.Protocol;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
public abstract class Event : IEvent {
public long EventId { get; }
public DateTimeOffset DateTime { get; }
public long UserId { get; }
public string ChannelId { get; }
public string SessionId { get; }
public string ConnectionId { get; }
public Event(
long eventId,
DateTimeOffset dateTime,
long userId,
string channelId,
string sessionId,
string connectionId
) {
EventId = eventId;
DateTime = dateTime;
UserId = userId;
ChannelId = channelId ?? string.Empty;
SessionId = sessionId ?? string.Empty;
ConnectionId = connectionId ?? string.Empty;
}
public Event(DateTimeOffset dateTime, long userId, string channelId, string sessionId, string connectionId)
: this(SharpId.Next(), dateTime, userId, channelId, sessionId, connectionId) { }
public Event(long userId, string channelId, string sessionId, string connectionId)
: this(DateTimeOffset.Now, userId, channelId, sessionId, connectionId) { }
public Event(string channelName, string sessionId, string connectionId)
: this(-1L, channelName, sessionId, connectionId) { }
public Event(IUser user, IChannel channel, ISession session, IConnection connection)
: this(user?.UserId ?? -1L, channel?.ChannelId, session?.SessionId, connection?.ConnectionId) { }
public Event(IUser user, ISession session, IConnection connection)
: this(user, null, session, connection) { }
public Event(IUser user, IChannel channel, ISession session)
: this(user, channel, session, session?.Connection) { }
public Event(IUser user, IChannel channel)
: this(user, channel, null, null) { }
public Event(long userId, IChannel channel)
: this(userId, channel.ChannelId, null, null) { }
public Event(IChannel channel, ISession session)
: this(session?.User, channel, session, session?.Connection) { }
public Event(ISession session, IConnection connection)
: this(session?.User, null, session, connection) { }
public Event(IUser user)
: this(user, null, null, null) { }
public Event(long userId)
: this(userId, null, null, null) { }
public Event(IChannel channel)
: this(null, channel, null, null) { }
public Event(ISession session)
: this(session?.User, null, session, session?.Connection) { }
public Event(IConnection connection)
: this(connection?.Session?.User, null, connection?.Session, connection) { }
public Event()
: this(-1L, null, null, null) { }
public override string ToString()
=> $@"[{EventId}:{GetType().Name}] U:{UserId} Ch:{ChannelId} S:{SessionId} Co:{ConnectionId}";
}
}

View file

@ -1,12 +0,0 @@
using System;
namespace SharpChat.Events {
[AttributeUsage(AttributeTargets.Class)]
public class EventAttribute : Attribute {
public string Type { get; }
public EventAttribute(string type) {
Type = type ?? throw new ArgumentNullException(nameof(type));
}
}
}

View file

@ -1,103 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace SharpChat.Events {
public class EventDispatcher : IEventDispatcher {
private Queue<(object sender, IEvent evt)> EventQueue { get; } = new();
private object SyncQueue { get; } = new();
private HashSet<IEventHandler> EventHandlers { get; } = new();
private object SyncHandlers { get; } = new();
private HashSet<IEventHandler> PreventDelete { get; } = new();
private object SyncPrevent { get; } = new();
private bool IsRunning = false;
private bool RunUntilEmpty = false;
private Thread ProcessThread = null;
[Conditional(@"DEBUG")]
private static void WithDebugColour(string str, ConsoleColor colour) {
ConsoleColor prev = Console.ForegroundColor;
Console.ForegroundColor = colour;
Logger.Debug(str);
Console.ForegroundColor = prev;
}
public void DispatchEvent(object sender, IEvent evt) {
lock(SyncQueue) {
WithDebugColour($@"+ {evt} <- {sender}.", ConsoleColor.Red);
EventQueue.Enqueue((sender, evt));
}
}
public void AddEventHandler(IEventHandler handler) {
if(handler == null)
throw new ArgumentNullException(nameof(handler));
lock(SyncHandlers)
EventHandlers.Add(handler);
}
internal void ProtectEventHandler(IEventHandler handler) {
lock(SyncPrevent)
PreventDelete.Add(handler);
}
public void RemoveEventHandler(IEventHandler handler) {
if(handler == null)
throw new ArgumentNullException(nameof(handler));
// prevent asshattery
lock(SyncPrevent)
if(PreventDelete.Contains(handler))
return;
lock(SyncHandlers)
EventHandlers.Remove(handler);
}
public bool ProcessNextQueue() {
(object sender, IEvent evt) queued;
lock(SyncQueue) {
if(!EventQueue.TryDequeue(out queued))
return false;
WithDebugColour($@"~ {queued.evt} <- {queued.sender}.", ConsoleColor.Green);
}
lock(SyncHandlers)
foreach(IEventHandler handler in EventHandlers)
handler.HandleEvent(queued.sender, queued.evt);
return true;
}
public void StartProcessing() {
if(IsRunning)
return;
IsRunning = true;
ProcessThread = new Thread(() => {
bool hadEvent;
do {
hadEvent = ProcessNextQueue();
if(RunUntilEmpty && !hadEvent)
StopProcessing();
else
Thread.Sleep(1);
} while(IsRunning);
});
ProcessThread.Start();
}
public void FinishProcessing() {
RunUntilEmpty = true;
ProcessThread.Join();
}
public void StopProcessing() {
IsRunning = false;
RunUntilEmpty = false;
}
}
}

View file

@ -1,12 +0,0 @@
using System;
namespace SharpChat.Events {
public interface IEvent {
long EventId { get; }
DateTimeOffset DateTime { get; }
long UserId { get; }
string ChannelId { get; }
string SessionId { get; }
string ConnectionId { get; }
}
}

View file

@ -1,7 +0,0 @@
namespace SharpChat.Events {
public interface IEventDispatcher {
void AddEventHandler(IEventHandler handler);
void RemoveEventHandler(IEventHandler handler);
void DispatchEvent(object sender, IEvent evt);
}
}

View file

@ -1,6 +0,0 @@
namespace SharpChat.Events {
public static class IEventExtensions {
public static bool IsBroadcast(this IEvent evt)
=> evt.ChannelId == null;
}
}

View file

@ -1,5 +0,0 @@
namespace SharpChat.Events {
public interface IEventHandler {
void HandleEvent(object sender, IEvent evt);
}
}

View file

@ -1,15 +0,0 @@
using System;
using System.Net;
namespace SharpChat.Events {
[Event(TYPE)]
public class IPBanRemovedEvent : Event {
public const string TYPE = @"ban:remove:ip";
public IPAddress IPAddress { get; }
public IPBanRemovedEvent(IPAddress ipAddress) : base() {
IPAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress));
}
}
}

View file

@ -1,34 +0,0 @@
using SharpChat.Messages;
using SharpChat.Sessions;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class MessageCreateEvent : Event {
public const string TYPE = @"message:create";
public long MessageId { get; }
public string Text { get; }
public bool IsAction { get; }
public string UserName { get; }
public Colour UserColour { get; }
public int UserRank { get; }
public string UserNickName { get; }
public UserPermissions UserPermissions { get; }
public MessageCreateEvent(ISession session, IMessage message)
: base(message.Channel, session) {
MessageId = message.MessageId;
Text = message.Text;
IsAction = message.IsAction;
UserName = message.Sender.UserName;
UserColour = message.Sender.Colour;
UserRank = message.Sender.Rank;
UserNickName = message.Sender is ILocalUser localUser && !string.IsNullOrWhiteSpace(localUser.NickName)
? localUser.NickName
: null;
UserPermissions = message.Sender.Permissions;
}
}
}

View file

@ -1,21 +0,0 @@
using SharpChat.Messages;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class MessageDeleteEvent : Event {
public const string TYPE = @"message:delete";
public long MessageId { get; }
public MessageDeleteEvent(IUser actor, IMessage message)
: base(actor, message.Channel) {
MessageId = message.MessageId;
}
public MessageDeleteEvent(MessageUpdateEvent mue)
: base(mue.UserId, mue.ChannelId, null, null) {
MessageId = mue.MessageId;
}
}
}

View file

@ -1,22 +0,0 @@
using SharpChat.Messages;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class MessageUpdateEvent : Event {
public const string TYPE = @"message:update";
public long MessageId { get; }
public string Text { get; }
public bool HasText
=> !string.IsNullOrEmpty(Text);
public MessageUpdateEvent(IMessage message, IUser editor, string text)
: base(editor, message.Channel) {
MessageId = message.MessageId;
Text = text ?? throw new ArgumentNullException(nameof(text));
}
}
}

View file

@ -1,12 +0,0 @@
using SharpChat.Channels;
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionChannelSwitchEvent : Event {
public const string TYPE = @"session:channel:switch";
public SessionChannelSwitchEvent(IChannel channel, ISession session)
: base(channel, session) { }
}
}

View file

@ -1,24 +0,0 @@
using SharpChat.Sessions;
using System;
using System.Net;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionCreatedEvent : Event {
public const string TYPE = @"session:create";
public string ServerId { get; }
public DateTimeOffset LastPing { get; }
public bool IsSecure { get; }
public bool IsConnected { get; }
public IPAddress RemoteAddress { get; }
public SessionCreatedEvent(ISession session) : base(session) {
ServerId = session.ServerId;
LastPing = session.LastPing;
IsSecure = session.IsSecure;
IsConnected = session.IsConnected;
RemoteAddress = session.RemoteAddress;
}
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionDestroyEvent : Event {
public const string TYPE = @"session:destroy";
public SessionDestroyEvent(ISession session)
: base(session) {}
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionPingEvent : Event {
public const string TYPE = @"session:ping";
public SessionPingEvent(ISession session)
: base(session) { }
}
}

View file

@ -1,29 +0,0 @@
using SharpChat.Protocol;
using SharpChat.Sessions;
using System;
using System.Net;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionResumeEvent : Event {
public const string TYPE = @"session:resume";
public string ServerId { get; }
public IPAddress RemoteAddress { get; }
public bool HasConnection
=> ConnectionId != null;
public SessionResumeEvent(ISession session, string serverId, IPAddress remoteAddress)
: base(session) {
ServerId = serverId ?? throw new ArgumentNullException(nameof(serverId));
RemoteAddress = remoteAddress ?? throw new ArgumentNullException(nameof(remoteAddress));
}
public SessionResumeEvent(ISession session, IConnection connection, string serverId)
: base(session, connection) {
ServerId = serverId ?? throw new ArgumentNullException(nameof(serverId));
RemoteAddress = connection?.RemoteAddress ?? throw new ArgumentNullException(nameof(connection));
}
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionSuspendEvent : Event {
public const string TYPE = @"session:suspend";
public SessionSuspendEvent(ISession session)
: base(session) { }
}
}

View file

@ -1,29 +0,0 @@
using SharpChat.Users.Remote;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserBanCreatedEvent : Event {
public const string TYPE = @"ban:create";
public long ModeratorUserId { get; }
public bool IsPermanent { get; }
public long Duration { get; }
public string Reason { get; }
public UserBanCreatedEvent(
IRemoteUser subject,
IRemoteUser moderator,
bool permanent,
TimeSpan duration,
string reason
) : base(
(subject ?? throw new ArgumentNullException(nameof(subject))).UserId
) {
ModeratorUserId = moderator?.UserId ?? -1;
IsPermanent = permanent;
Duration = (long)duration.TotalSeconds;
Reason = reason ?? throw new ArgumentNullException(nameof(reason));
}
}
}

View file

@ -1,14 +0,0 @@
using SharpChat.Users.Remote;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserBanRemovedEvent : Event {
public const string TYPE = @"ban:remove:user";
public UserBanRemovedEvent(IRemoteUser remoteUser)
: base(
(remoteUser ?? throw new ArgumentNullException(nameof(remoteUser))).UserId
) { }
}
}

View file

@ -1,28 +0,0 @@
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserConnectEvent : Event {
public const string TYPE = @"user:connect";
public string Name { get; }
public Colour Colour { get; }
public int Rank { get; }
public UserPermissions Permissions { get; }
public string NickName { get; }
public UserStatus Status { get; }
public string StatusMessage { get; }
public UserConnectEvent(ILocalUser user)
: base(user ?? throw new ArgumentNullException(nameof(user))) {
Name = user.UserName;
Colour = user.Colour;
Rank = user.Rank;
Permissions = user.Permissions;
NickName = string.IsNullOrWhiteSpace(user.NickName) ? null : user.NickName;
Status = user.Status;
StatusMessage = user.StatusMessage;
}
}
}

View file

@ -1,16 +0,0 @@
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserDisconnectEvent : Event {
public const string TYPE = @"user:disconnect";
public UserDisconnectReason Reason { get; }
public UserDisconnectEvent(IUser user, UserDisconnectReason reason)
: base(user ?? throw new ArgumentNullException(nameof(user))) {
Reason = reason;
}
}
}

View file

@ -1,85 +0,0 @@
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserUpdateEvent : Event {
public const string TYPE = @"user:update";
public string OldUserName { get; }
public string NewUserName { get; }
public Colour OldColour { get; }
public Colour? NewColour { get; }
public int? OldRank { get; }
public int? NewRank { get; }
public string OldNickName { get; }
public string NewNickName { get; }
public UserPermissions OldPerms { get; }
public UserPermissions? NewPerms { get; }
public UserStatus OldStatus { get; }
public UserStatus? NewStatus { get; }
public string OldStatusMessage { get; }
public string NewStatusMessage { get; }
public bool HasUserName => NewUserName != null;
public bool HasNickName => NewNickName != null;
public bool HasStatusMessage => NewStatusMessage != null;
public UserUpdateEvent(
ILocalUser user,
string userName = null,
Colour? colour = null,
int? rank = null,
string nickName = null,
UserPermissions? perms = null,
UserStatus? status = null,
string statusMessage = null
) : base(user ?? throw new ArgumentNullException(nameof(user))) {
OldUserName = user.UserName;
if(!OldUserName.Equals(userName))
NewUserName = userName;
OldColour = user.Colour;
if(!OldColour.Equals(colour))
NewColour = colour;
OldRank = user.Rank;
if(OldRank != rank)
NewRank = rank;
OldNickName = user.NickName;
if(!OldNickName.Equals(nickName))
NewNickName = nickName;
OldPerms = user.Permissions;
if(OldPerms != perms)
NewPerms = perms;
OldStatus = user.Status;
if(OldStatus != status)
NewStatus = status;
OldStatusMessage = user.StatusMessage;
if(!OldStatusMessage.Equals(statusMessage))
NewStatusMessage = statusMessage;
}
public UserUpdateEvent(ILocalUser user, UserUpdateEvent uue)
: this(
user,
uue.NewUserName,
uue.NewColour,
uue.NewRank,
uue.NewNickName,
uue.NewPerms,
uue.NewStatus,
uue.NewStatusMessage
) { }
}
}

View file

@ -1,16 +0,0 @@
using SharpChat.Channels;
using SharpChat.Users;
using System;
namespace SharpChat.Messages {
public interface IMessage {
long MessageId { get; }
IChannel Channel { get; }
IUser Sender { get; }
string Text { get; }
bool IsAction { get; }
DateTimeOffset Created { get; }
DateTimeOffset? Edited { get; }
bool IsEdited { get; }
}
}

View file

@ -1,10 +0,0 @@
namespace SharpChat.Messages {
public static class IMessageExtensions {
public static string GetSanitisedText(this IMessage msg)
=> msg.Text
.Replace(@"<", @"&lt;")
.Replace(@">", @"&gt;")
.Replace("\n", @" <br/> ")
.Replace("\t", @" ");
}
}

View file

@ -1,34 +0,0 @@
using SharpChat.Channels;
using SharpChat.Users;
using System;
namespace SharpChat.Messages {
public class Message : IMessage {
public long MessageId { get; }
public IChannel Channel { get; }
public IUser Sender { get; }
public string Text { get; }
public bool IsAction { get; }
public DateTimeOffset Created { get; }
public DateTimeOffset? Edited { get; }
public bool IsEdited => Edited.HasValue;
public Message(
IChannel channel,
IUser sender,
string text,
bool isAction = false,
DateTimeOffset? created = null,
DateTimeOffset? edited = null
) {
MessageId = SharpId.Next();
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
Sender = sender ?? throw new ArgumentNullException(nameof(sender));
Text = text ?? throw new ArgumentNullException(nameof(text));
IsAction = isAction;
Created = created ?? DateTimeOffset.Now;
Edited = edited;
}
}
}

View file

@ -1,99 +0,0 @@
using SharpChat.Channels;
using SharpChat.Configuration;
using SharpChat.Events;
using SharpChat.Messages.Storage;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
using System.Collections.Generic;
namespace SharpChat.Messages {
public class MessageManager : IEventHandler {
private IEventDispatcher Dispatcher { get; }
private IMessageStorage Storage { get; }
private IConfig Config { get; }
public const int DEFAULT_LENGTH_MAX = 2100;
private CachedValue<int> TextMaxLengthValue { get; }
public int TextMaxLength => TextMaxLengthValue;
public MessageManager(IEventDispatcher dispatcher, IMessageStorage storage, IConfig config) {
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Storage = storage ?? throw new ArgumentNullException(nameof(storage));
Config = config ?? throw new ArgumentNullException(nameof(config));
TextMaxLengthValue = Config.ReadCached(@"maxLength", DEFAULT_LENGTH_MAX);
}
public Message Create(ISession session, IChannel channel, string text, bool isAction = false)
=> Create(session, session.User, channel, text, isAction);
public Message Create(ISession session, IUser sender, IChannel channel, string text, bool isAction = false) {
if(session == null)
throw new ArgumentNullException(nameof(session));
if(sender == null)
throw new ArgumentNullException(nameof(sender));
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(text == null)
throw new ArgumentNullException(nameof(text));
if(string.IsNullOrWhiteSpace(text))
throw new ArgumentException(@"Provided text is empty.", nameof(text));
if(text.Length > TextMaxLength)
throw new ArgumentException(@"Provided text is too long.", nameof(text));
Message message = new(channel, sender, text, isAction);
Dispatcher.DispatchEvent(this, new MessageCreateEvent(session, message));
return message;
}
public void Edit(IUser editor, IMessage message, string text = null) {
if(editor == null)
throw new ArgumentNullException(nameof(editor));
if(message == null)
throw new ArgumentNullException(nameof(message));
if(text == null)
return;
if(string.IsNullOrWhiteSpace(text))
throw new ArgumentException(@"Provided text is empty.", nameof(text));
if(text.Length > TextMaxLength)
throw new ArgumentException(@"Provided text is too long.", nameof(text));
MessageUpdateEvent mue = new(message, editor, text);
if(message is IEventHandler meh)
meh.HandleEvent(this, mue);
Dispatcher.DispatchEvent(this, mue);
}
public void Delete(IUser user, IMessage message) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(message == null)
throw new ArgumentNullException(nameof(message));
MessageDeleteEvent mde = new(user, message);
if(message is IEventHandler meh)
meh.HandleEvent(this, mde);
Dispatcher.DispatchEvent(this, mde);
}
public void GetMessage(long messageId, Action<IMessage> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Storage.GetMessage(messageId, callback);
}
public void GetMessages(IChannel channel, Action<IEnumerable<IMessage>> callback, int amount = 20, int offset = 0) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Storage.GetMessages(channel, callback, amount, offset);
}
public void HandleEvent(object sender, IEvent evt)
=> Storage.HandleEvent(sender, evt);
}
}

View file

@ -1,33 +0,0 @@
using SharpChat.Channels;
using SharpChat.Database;
using SharpChat.Users;
using System;
namespace SharpChat.Messages.Storage {
public class ADOMessage : IMessage {
public long MessageId { get; }
public IChannel Channel { get; }
public IUser Sender { get; }
public string Text { get; }
public DateTimeOffset Created { get; }
public DateTimeOffset? Edited { get; }
public bool IsAction => (Flags & IS_ACTION) == IS_ACTION;
public bool IsEdited => Edited.HasValue;
public const byte IS_ACTION = 1;
public byte Flags { get; }
public ADOMessage(IDatabaseReader reader) {
if(reader == null)
throw new ArgumentNullException(nameof(reader));
MessageId = reader.ReadI64(@"msg_id");
Channel = new ADOMessageChannel(reader);
Sender = new ADOMessageUser(reader);
Text = reader.ReadString(@"msg_text");
Flags = reader.ReadU8(@"msg_flags");
Created = DateTimeOffset.FromUnixTimeSeconds(reader.ReadI64(@"msg_created"));
Edited = reader.IsNull(@"msg_edited") ? null : DateTimeOffset.FromUnixTimeSeconds(reader.ReadI64(@"msg_edited"));
}
}
}

View file

@ -1,31 +0,0 @@
using SharpChat.Channels;
using SharpChat.Database;
using System;
namespace SharpChat.Messages.Storage {
public class ADOMessageChannel : IChannel {
public string ChannelId { get; }
public string Name => string.Empty;
public string Topic => string.Empty;
public bool IsTemporary => true;
public int MinimumRank => 0;
public bool AutoJoin => false;
public uint MaxCapacity => 0;
public long OwnerId => -1;
public string Password => string.Empty;
public bool HasPassword => false;
public int Order => 0;
public ADOMessageChannel(IDatabaseReader reader) {
if(reader == null)
throw new ArgumentNullException(nameof(reader));
ChannelId = reader.ReadString(@"msg_channel_id");
}
public bool Equals(IChannel other)
=> other != null && ChannelId.Equals(other.ChannelId);
public override string ToString()
=> $@"<ADOMessageChannel {ChannelId}>";
}
}

View file

@ -1,137 +0,0 @@
using SharpChat.Channels;
using SharpChat.Database;
using SharpChat.Events;
using System;
using System.Collections.Generic;
namespace SharpChat.Messages.Storage {
public partial class ADOMessageStorage : IMessageStorage {
private DatabaseWrapper Wrapper { get; }
public ADOMessageStorage(DatabaseWrapper wrapper) {
Wrapper = wrapper ?? throw new ArgumentNullException(nameof(wrapper));
RunMigrations();
}
public void GetMessage(long messageId, Action<IMessage> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
IMessage msg = null;
Wrapper.RunQuery(
@"SELECT `msg_id`, `msg_channel_id`, `msg_sender_id`, `msg_sender_name`, `msg_sender_colour`, `msg_sender_rank`, `msg_sender_nick`"
+ @", `msg_sender_perms`, `msg_text`, `msg_flags`"
+ @", " + Wrapper.ToUnixTime(@"`msg_created`") + @" AS `msg_created`"
+ @", " + Wrapper.ToUnixTime(@"`msg_edited`") + @" AS `msg_edited`"
+ @" FROM `sqc_messages`"
+ @" WHERE `msg_id` = @id"
+ @" AND `msg_deleted` IS NULL"
+ @" LIMIT 1",
reader => {
if(reader.Next())
msg = new ADOMessage(reader);
},
Wrapper.CreateParam(@"id", messageId)
);
callback(msg);
}
public void GetMessages(IChannel channel, Action<IEnumerable<IMessage>> callback, int amount, int offset) {
List<IMessage> msgs = new();
Wrapper.RunQuery(
@"SELECT `msg_id`, `msg_channel_id`, `msg_sender_id`, `msg_sender_name`, `msg_sender_colour`, `msg_sender_rank`, `msg_sender_nick`"
+ @", `msg_sender_perms`, `msg_text`, `msg_flags`"
+ @", " + Wrapper.ToUnixTime(@"`msg_created`") + @" AS `msg_created`"
+ @", " + Wrapper.ToUnixTime(@"`msg_edited`") + @" AS `msg_edited`"
+ @" FROM `sqc_messages`"
+ @" WHERE `msg_channel_id` = @channelId"
+ @" AND `msg_deleted` IS NULL"
+ @" ORDER BY `msg_id` DESC"
+ @" LIMIT @amount OFFSET @offset",
reader => {
while(reader.Next())
msgs.Add(new ADOMessage(reader));
},
Wrapper.CreateParam(@"channelId", channel.ChannelId),
Wrapper.CreateParam(@"amount", amount),
Wrapper.CreateParam(@"offset", offset)
);
msgs.Reverse();
callback(msgs);
}
private void StoreMessage(MessageCreateEvent mce) {
byte flags = 0;
if(mce.IsAction)
flags |= ADOMessage.IS_ACTION;
Wrapper.RunCommand(
@"INSERT INTO `sqc_messages` ("
+ @"`msg_id`, `msg_channel_id`, `msg_sender_id`, `msg_sender_name`, `msg_sender_colour`, `msg_sender_rank`"
+ @", `msg_sender_nick`, `msg_sender_perms`, `msg_text`, `msg_flags`, `msg_created`"
+ @") VALUES ("
+ @"@id, @channelId, @senderId, @senderName, @senderColour, @senderRank, @senderNick, @senderPerms"
+ @", @text, @flags, " + Wrapper.FromUnixTime(@"@created")
+ @");",
Wrapper.CreateParam(@"id", mce.MessageId),
Wrapper.CreateParam(@"channelId", mce.ChannelId),
Wrapper.CreateParam(@"senderId", mce.UserId),
Wrapper.CreateParam(@"senderName", mce.UserName),
Wrapper.CreateParam(@"senderColour", mce.UserColour.Raw),
Wrapper.CreateParam(@"senderRank", mce.UserRank),
Wrapper.CreateParam(@"senderNick", mce.UserNickName),
Wrapper.CreateParam(@"senderPerms", mce.UserPermissions),
Wrapper.CreateParam(@"text", mce.Text),
Wrapper.CreateParam(@"flags", flags),
Wrapper.CreateParam(@"created", mce.DateTime.ToUnixTimeSeconds())
);
}
private void UpdateMessage(MessageUpdateEvent mue) {
Wrapper.RunCommand(
@"UPDATE `sqc_messages` SET `msg_text` = @text, `msg_edited` = " + Wrapper.FromUnixTime(@"@edited") + @" WHERE `msg_id` = @id",
Wrapper.CreateParam(@"text", mue.Text),
Wrapper.CreateParam(@"edited", mue.DateTime.ToUnixTimeSeconds()),
Wrapper.CreateParam(@"id", mue.MessageId)
);
}
private void DeleteMessage(MessageDeleteEvent mde) {
Wrapper.RunCommand(
@"UPDATE `sqc_messages` SET `msg_deleted` = " + Wrapper.FromUnixTime(@"@deleted") + @" WHERE `msg_id` = @id",
Wrapper.CreateParam(@"deleted", mde.DateTime.ToUnixTimeSeconds()),
Wrapper.CreateParam(@"id", mde.MessageId)
);
}
private void DeleteChannel(ChannelDeleteEvent cde) {
Wrapper.RunCommand(
@"UPDATE `sqc_messages` SET `msg_deleted` = " + Wrapper.FromUnixTime(@"@deleted") + @" WHERE `msg_channel_id` = @channelId AND `msg_deleted` IS NULL",
Wrapper.CreateParam(@"deleted", cde.DateTime.ToUnixTimeSeconds()),
Wrapper.CreateParam(@"channelId", cde.ChannelId)
);
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case MessageCreateEvent mce:
StoreMessage(mce);
break;
case MessageUpdateEvent mue:
UpdateMessage(mue);
break;
case MessageDeleteEvent mde:
DeleteMessage(mde);
break;
case ChannelDeleteEvent cde:
DeleteChannel(cde);
break;
}
}
}
}

View file

@ -1,73 +0,0 @@
using System;
namespace SharpChat.Messages.Storage {
public partial class ADOMessageStorage {
private const string CREATE_MESSAGES_TABLE = @"create_msgs_table";
private const string LEGACY_CREATE_EVENTS_TABLE = @"create_events_table";
public void RunMigrations() {
Wrapper.RunCommand(
@"CREATE TABLE IF NOT EXISTS `sqc_migrations` ("
+ @"`migration_name` " + Wrapper.VarCharType(255) + @" PRIMARY KEY,"
+ @"`migration_completed` " + Wrapper.TimestampType + @" NOT NULL DEFAULT 0"
+ @");"
);
Wrapper.RunCommand(@"CREATE INDEX IF NOT EXISTS `sqc_migrations_completed_index` ON `sqc_migrations` (`migration_completed`);");
DoMigration(CREATE_MESSAGES_TABLE, CreateMessagesTable);
}
private bool CheckMigration(string name) {
return Wrapper.RunQueryValue(
@"SELECT `migration_completed` IS NOT NULL FROM `sqc_migrations` WHERE `migration_name` = @name LIMIT 1",
Wrapper.CreateParam(@"name", name)
) is not null;
}
private void DoMigration(string name, Action action) {
if(!CheckMigration(name)) {
Logger.Write($@"Running migration '{name}'...");
action();
Wrapper.RunCommand(
@"INSERT INTO `sqc_migrations` (`migration_name`, `migration_completed`) VALUES (@name, " + Wrapper.DateTimeNow() + @")",
Wrapper.CreateParam(@"name", name)
);
}
}
private void CreateMessagesTable() {
Wrapper.RunCommand(
@"CREATE TABLE `sqc_messages` ("
+ @"`msg_id` " + Wrapper.BigIntType(20) + @" PRIMARY KEY,"
+ @"`msg_channel_id` " + Wrapper.VarBinaryType(255) + @" NOT NULL,"
+ @"`msg_sender_id` " + Wrapper.BigUIntType(20) + @" NOT NULL,"
+ @"`msg_sender_name` " + Wrapper.VarCharType(255) + @" NOT NULL COLLATE " + Wrapper.UnicodeCollation + @","
+ @"`msg_sender_colour` " + Wrapper.IntType(11) + @" NOT NULL,"
+ @"`msg_sender_rank` " + Wrapper.IntType(11) + @" NOT NULL,"
+ @"`msg_sender_nick` " + Wrapper.VarCharType(255) + @" NULL DEFAULT NULL COLLATE " + Wrapper.UnicodeCollation + @","
+ @"`msg_sender_perms` " + Wrapper.IntType(11) + @" NOT NULL,"
+ @"`msg_text` " + Wrapper.TextType + @" NOT NULL COLLATE " + Wrapper.UnicodeCollation + @","
+ @"`msg_flags` " + Wrapper.TinyUIntType(3) + @" NOT NULL,"
+ @"`msg_created` " + Wrapper.TimestampType + @" NOT NULL DEFAULT 0,"
+ @"`msg_edited` " + Wrapper.TimestampType + @" NULL DEFAULT NULL,"
+ @"`msg_deleted` " + Wrapper.TimestampType + @" NULL DEFAULT NULL"
+ @");"
);
Wrapper.RunCommand(@"CREATE INDEX `sqc_messages_channel_index` ON `sqc_messages` (`msg_channel_id`);");
Wrapper.RunCommand(@"CREATE INDEX `sqc_messages_sender_index` ON `sqc_messages` (`msg_sender_id`);");
Wrapper.RunCommand(@"CREATE INDEX `sqc_messages_flags_index` ON `sqc_messages` (`msg_flags`);");
Wrapper.RunCommand(@"CREATE INDEX `sqc_messages_created_index` ON `sqc_messages` (`msg_created`);");
Wrapper.RunCommand(@"CREATE INDEX `sqc_messages_edited_index` ON `sqc_messages` (`msg_edited`);");
Wrapper.RunCommand(@"CREATE INDEX `sqc_messages_deleted_index` ON `sqc_messages` (`msg_deleted`);");
if(Wrapper.SupportsJson && CheckMigration(LEGACY_CREATE_EVENTS_TABLE))
Wrapper.RunCommand(
@"INSERT INTO `sqc_messages`"
+ @" SELECT `event_id`, " + Wrapper.ToLower(@"`event_target`") + @", `event_sender`, `event_sender_name`"
+ @", `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
+ @", " + Wrapper.JsonValue(@"`event_data`", @"$.text") + @", `event_flags` & 1, `event_created`, NULL, `event_deleted`"
+ @" FROM `sqc_events` WHERE `event_type` = 'SharpChat.Events.ChatMessage';", 1800
);
}
}
}

View file

@ -1,33 +0,0 @@
using SharpChat.Database;
using SharpChat.Users;
using System;
namespace SharpChat.Messages.Storage {
public class ADOMessageUser : IUser {
public long UserId { get; }
public string UserName { get; }
public Colour Colour { get; }
public int Rank { get; }
public string NickName { get; }
public UserPermissions Permissions { get; }
public UserStatus Status => UserStatus.Unknown;
public string StatusMessage => string.Empty;
public ADOMessageUser(IDatabaseReader reader) {
if(reader == null)
throw new ArgumentNullException(nameof(reader));
UserId = reader.ReadI64(@"msg_sender_id");
UserName = reader.ReadString(@"msg_sender_name");
Colour = new(reader.ReadI32(@"msg_sender_colour"));
Rank = reader.ReadI32(@"msg_sender_rank");
NickName = reader.IsNull(@"msg_sender_nick") ? null : reader.ReadString(@"msg_sender_nick");
Permissions = (UserPermissions)reader.ReadI32(@"msg_sender_perms");
}
public bool Equals(IUser other)
=> other != null && other.UserId == UserId;
public override string ToString()
=> $@"<ADOMessageUser {UserId}#{UserName}>";
}
}

View file

@ -1,11 +0,0 @@
using SharpChat.Channels;
using SharpChat.Events;
using System;
using System.Collections.Generic;
namespace SharpChat.Messages.Storage {
public interface IMessageStorage : IEventHandler {
void GetMessage(long messageId, Action<IMessage> callback);
void GetMessages(IChannel channel, Action<IEnumerable<IMessage>> callback, int amount, int offset);
}
}

View file

@ -1,42 +0,0 @@
using SharpChat.Channels;
using SharpChat.Events;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharpChat.Messages.Storage {
public class MemoryMessage : IMessage, IEventHandler {
public long MessageId { get; }
public IChannel Channel { get; }
public IUser Sender { get; }
public string Text { get; private set; }
public bool IsAction { get; }
public DateTimeOffset Created { get; }
public DateTimeOffset? Edited { get; private set; }
public bool IsEdited => Edited.HasValue;
public MemoryMessage(MemoryMessageChannel channel, MessageCreateEvent mce) {
if(mce == null)
throw new ArgumentNullException(nameof(mce));
MessageId = mce.MessageId;
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
Sender = new MemoryMessageUser(mce);
Text = mce.Text;
IsAction = mce.IsAction;
Created = mce.DateTime;
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case MessageUpdateEvent mue:
Edited = mue.DateTime;
if(mue.HasText)
Text = mue.Text;
break;
}
}
}
}

View file

@ -1,28 +0,0 @@
using SharpChat.Channels;
using SharpChat.Events;
namespace SharpChat.Messages.Storage {
public class MemoryMessageChannel : IChannel {
public string ChannelId { get; }
public string Name => string.Empty;
public string Topic => string.Empty;
public bool IsTemporary => true;
public int MinimumRank => 0;
public bool AutoJoin => false;
public uint MaxCapacity => 0;
public long OwnerId => -1;
public string Password => string.Empty;
public bool HasPassword => false;
public int Order => 0;
public MemoryMessageChannel(IEvent evt) {
ChannelId = evt.ChannelId;
}
public bool Equals(IChannel other)
=> other != null && ChannelId.Equals(other.ChannelId);
public override string ToString()
=> $@"<MemoryMessageChannel {ChannelId}>";
}
}

View file

@ -1,90 +0,0 @@
using SharpChat.Channels;
using SharpChat.Events;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Messages.Storage {
public class MemoryMessageStorage : IMessageStorage {
private List<MemoryMessage> Messages { get; } = new();
private List<MemoryMessageChannel> Channels { get; } = new();
private readonly object Sync = new();
public void GetMessage(long messageId, Action<IMessage> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Messages.FirstOrDefault(m => m.MessageId == messageId));
}
public void GetMessages(IChannel channel, Action<IEnumerable<IMessage>> callback, int amount, int offset) {
lock(Sync) {
IEnumerable<IMessage> subset = Messages.Where(m => m.Channel.Equals(channel));
int start = subset.Count() - offset - amount;
if(start < 0) {
amount += start;
start = 0;
}
callback(subset.Skip(start).Take(amount));
}
}
private void StoreMessage(MessageCreateEvent mce) {
lock(Sync) {
MemoryMessageChannel channel = Channels.FirstOrDefault(c => mce.ChannelId.Equals(mce.ChannelId));
if(channel == null)
return; // This is basically an invalid state
Messages.Add(new(channel, mce));
}
}
private void UpdateMessage(object sender, MessageUpdateEvent mue) {
lock(Sync)
Messages.FirstOrDefault(m => m.MessageId == mue.MessageId)?.HandleEvent(sender, mue);
}
private void DeleteMessage(MessageDeleteEvent mde) {
lock(Sync)
Messages.RemoveAll(m => m.MessageId == mde.MessageId);
}
private void CreateChannel(ChannelCreateEvent cce) {
lock(Sync)
Channels.Add(new(cce));
}
private void DeleteChannel(ChannelDeleteEvent cde) {
lock(Sync) {
MemoryMessageChannel channel = Channels.FirstOrDefault(c => cde.ChannelId.Equals(c.ChannelId));
if(channel == null)
return;
Channels.Remove(channel);
Messages.RemoveAll(m => m.Channel.Equals(channel));
}
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case MessageCreateEvent mce:
StoreMessage(mce);
break;
case MessageUpdateEvent mue:
UpdateMessage(sender, mue);
break;
case MessageDeleteEvent mde:
DeleteMessage(mde);
break;
case ChannelCreateEvent cce:
CreateChannel(cce);
break;
case ChannelDeleteEvent cde:
DeleteChannel(cde);
break;
}
}
}
}

View file

@ -1,30 +0,0 @@
using SharpChat.Events;
using SharpChat.Users;
namespace SharpChat.Messages.Storage {
public class MemoryMessageUser : IUser {
public long UserId { get; }
public string UserName { get; }
public Colour Colour { get; }
public int Rank { get; }
public string NickName { get; }
public UserPermissions Permissions { get; }
public UserStatus Status => UserStatus.Unknown;
public string StatusMessage => string.Empty;
public MemoryMessageUser(MessageCreateEvent mce) {
UserId = mce.UserId;
UserName = mce.UserName;
Colour = mce.UserColour;
Rank = mce.UserRank;
NickName = mce.UserNickName;
Permissions = mce.UserPermissions;
}
public bool Equals(IUser other)
=> other != null && other.UserId == UserId;
public override string ToString()
=> $@"<MemoryMessageUser {UserId}#{UserName}>";
}
}

View file

@ -1,170 +0,0 @@
using SharpChat.Channels;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Protocol {
public class ConnectionList<TConnection>
where TConnection : IConnection {
private HashSet<TConnection> Connections { get; } = new();
private readonly object Sync = new();
private SessionManager Sessions { get; }
private ChannelUserRelations ChannelUsers { get; }
public ConnectionList(
SessionManager sessions,
ChannelUserRelations channelUsers
) {
Sessions = sessions ?? throw new ArgumentNullException(nameof(sessions));
ChannelUsers = channelUsers ?? throw new ArgumentNullException(nameof(channelUsers));
}
public virtual void AddConnection(TConnection conn) {
if(conn == null)
throw new ArgumentNullException(nameof(conn));
lock(Sync)
Connections.Add(conn);
}
public virtual void RemoveConnection(TConnection conn) {
if(conn == null)
throw new ArgumentNullException(nameof(conn));
lock(Sync)
Connections.Remove(conn);
}
public void RemoveConnection(string connId) {
if(connId == null)
throw new ArgumentNullException(nameof(connId));
GetConnection(connId, c => Connections.Remove(c));
}
public void GetConnection(Func<TConnection, bool> predicate, Action<TConnection> callback) {
if(predicate == null)
throw new ArgumentNullException(nameof(predicate));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync) {
TConnection conn = Connections.FirstOrDefault(predicate);
if(conn == null)
return;
callback(conn);
}
}
public void GetConnection(string connId, Action<TConnection> callback) {
if(connId == null)
throw new ArgumentNullException(nameof(connId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetConnection(c => connId.Equals(c.ConnectionId), callback);
}
public void GetConnection(ISession session, Action<TConnection> callback) {
if(session == null)
throw new ArgumentNullException(nameof(session));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetConnection(c => session.Equals(c.Session), callback);
}
public void GetConnectionBySessionId(string sessionId, Action<TConnection> callback) {
if(sessionId == null)
throw new ArgumentNullException(nameof(sessionId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(sessionId)) {
callback(default);
return;
}
GetConnection(c => c.Session != null && sessionId.Equals(c.Session.SessionId), callback);
}
public void GetConnections(Func<TConnection, bool> predicate, Action<IEnumerable<TConnection>> callback) {
if(predicate == null)
throw new ArgumentNullException(nameof(predicate));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Connections.Where(predicate));
}
public void GetConnectionsWithSession(Action<IEnumerable<TConnection>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetConnections(c => c.Session != null, callback);
}
public void GetOwnConnections(IUser user, Action<IEnumerable<TConnection>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetConnections(c => c.Session != null && user.Equals(c.Session.User), callback);
}
public void GetConnectionsByChannelId(string channelId, Action<IEnumerable<TConnection>> callback) {
if(channelId == null)
throw new ArgumentNullException(nameof(channelId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
ChannelUsers.GetLocalSessionsByChannelId(channelId, sessions => GetConnections(sessions, callback));
}
public void GetConnectionsByChannelName(string channelName, Action<IEnumerable<TConnection>> callback) {
if(channelName == null)
throw new ArgumentNullException(nameof(channelName));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
ChannelUsers.GetLocalSessionsByChannelName(channelName, sessions => GetConnections(sessions, callback));
}
public void GetConnections(IChannel channel, Action<IEnumerable<TConnection>> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
ChannelUsers.GetLocalSessions(channel, sessions => GetConnections(sessions, callback));
}
public void GetConnections(IEnumerable<ISession> sessions, Action<IEnumerable<TConnection>> callback) {
if(sessions == null)
throw new ArgumentNullException(nameof(sessions));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(!sessions.Any()) {
callback(Enumerable.Empty<TConnection>());
return;
}
lock(Sync)
callback(sessions.Where(s => s.Connection is TConnection conn && Connections.Contains(conn)).Select(s => (TConnection)s.Connection));
}
public void GetAllConnections(IUser user, Action<IEnumerable<TConnection>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Sessions.GetLocalSessions(user, sessions => GetConnections(sessions, callback));
}
public void GetAllConnectionsByUserId(long userId, Action<IEnumerable<TConnection>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(userId < 1) {
callback(Enumerable.Empty<TConnection>());
return;
}
Sessions.GetLocalSessionsByUserId(userId, sessions => GetConnections(sessions, callback));
}
public void GetDeadConnections(Action<IEnumerable<TConnection>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetConnections(c => !c.IsAvailable, callback);
}
}
}

View file

@ -1,15 +0,0 @@
using SharpChat.Sessions;
using System;
using System.Net;
namespace SharpChat.Protocol {
public interface IConnection {
string ConnectionId { get; }
IPAddress RemoteAddress { get; }
bool IsAvailable { get; }
bool IsSecure { get; }
DateTimeOffset LastPing { get; set; }
ISession Session { get; set; }
void Close();
}
}

View file

@ -1,9 +0,0 @@
using SharpChat.Events;
using System;
using System.Net;
namespace SharpChat.Protocol {
public interface IServer : IEventHandler, IDisposable {
void Listen(EndPoint endPoint);
}
}

View file

@ -1,16 +0,0 @@
using SharpChat.Events;
using System;
using System.Net;
namespace SharpChat.Protocol.Null {
[Server(@"null")]
public class NullServer : IServer {
public void Listen(EndPoint endPoint) { }
public void HandleEvent(object sender, IEvent evt) { }
public void Dispose() {
GC.SuppressFinalize(this);
}
}
}

View file

@ -1,11 +0,0 @@
using System;
namespace SharpChat.Protocol {
public class ProtocolException : Exception {
public ProtocolException(string message) : base(message) { }
}
public class ProtocolAlreadyListeningException : ProtocolException {
public ProtocolAlreadyListeningException() : base(@"Protocol is already listening.") { }
}
}

View file

@ -1,8 +0,0 @@
using SharpChat.Reflection;
namespace SharpChat.Protocol {
public class ServerAttribute : ObjectConstructorAttribute {
public ServerAttribute(string name) : base(name) {
}
}
}

View file

@ -1,80 +0,0 @@
using SharpChat.Configuration;
using SharpChat.Protocol;
using SharpChat.Users;
using System;
using System.Collections.Generic;
namespace SharpChat.RateLimiting {
public class RateLimitManager {
public const int DEFAULT_USER_SIZE = 15;
public const int DEFAULT_USER_WARN_SIZE = 10;
public const int DEFAULT_CONN_SIZE = 30;
public const int DEFAULT_MINIMUM_DELAY = 5000;
public const int DEFAULT_KICK_LENGTH = 5;
public const int DEFAULT_KICK_MULTIPLIER = 2;
private CachedValue<int> UserSizeValue { get; }
private CachedValue<int> UserWarnSizeValue { get; }
private CachedValue<int> ConnSizeValue { get; }
private CachedValue<int> MinimumDelayValue { get; }
private CachedValue<int> KickLengthValue { get; }
private CachedValue<int> KickMultiplierValue { get; }
private readonly object ConnectionsSync = new();
private Dictionary<string, RateLimiter> Connections { get; } = new();
private readonly object UsersSync = new();
private Dictionary<long, RateLimiter> Users { get; } = new();
public RateLimitManager(IConfig config) {
UserSizeValue = config.ReadCached(@"userSize", DEFAULT_USER_SIZE);
UserWarnSizeValue = config.ReadCached(@"userWarnSize", DEFAULT_USER_WARN_SIZE);
ConnSizeValue = config.ReadCached(@"connSize", DEFAULT_CONN_SIZE);
MinimumDelayValue = config.ReadCached(@"minDelay", DEFAULT_MINIMUM_DELAY);
KickLengthValue = config.ReadCached(@"kickLength", DEFAULT_KICK_LENGTH);
KickMultiplierValue = config.ReadCached(@"kickMultiplier", DEFAULT_KICK_MULTIPLIER);
}
private RateLimiter CreateForConnection() {
return new RateLimiter(
ConnSizeValue,
-1,
MinimumDelayValue
);
}
private RateLimiter CreateForUser() {
return new RateLimiter(
UserSizeValue,
UserWarnSizeValue,
MinimumDelayValue
);
}
public TimeSpan GetKickLength(int kickCount) {
if(kickCount < 1)
kickCount = 1;
return TimeSpan.FromSeconds(KickLengthValue * (KickMultiplierValue * kickCount));
}
public bool UpdateConnection(IConnection conn) {
lock(ConnectionsSync) {
string connId = conn.ConnectionId;
if(!Connections.ContainsKey(connId))
Connections[connId] = CreateForConnection();
Connections[connId].Update();
return Connections[connId].ShouldKick;
}
}
public (bool kick, bool warn) UpdateUser(IUser user) {
lock(UsersSync) {
long userId = user.UserId;
if(!Users.ContainsKey(userId))
Users[userId] = CreateForUser();
Users[userId].Update();
return (Users[userId].ShouldKick, Users[userId].ShouldWarn);
}
}
}
}

View file

@ -1,38 +0,0 @@
using System;
namespace SharpChat.RateLimiting {
public class RateLimiter {
private int Size { get; }
private int WarnSize { get; }
private int MinimumDelay { get; }
private long[] Times { get; }
public RateLimiter(int size, int warnSize, int minimumDelay) {
if(size < 3)
throw new ArgumentException(@"Size must be more than 1.", nameof(size));
if(warnSize >= size && warnSize > 0)
throw new ArgumentException(@"Warning Size must be less than Size, or less than 0 to be disabled.", nameof(warnSize));
if(minimumDelay < 1000)
throw new ArgumentException(@"Minimum Delay must be more than 999 milliseconds.", nameof(minimumDelay));
Size = size;
WarnSize = warnSize;
MinimumDelay = minimumDelay;
Times = new long[Size];
}
private bool IsSeeding
=> (Times[0] < 1 && Times[1] < 1);
public bool ShouldKick
=> !IsSeeding && Times[0] + MinimumDelay >= Times[Size - 1];
public bool ShouldWarn
=> WarnSize > 0 && !IsSeeding && Times[0] + MinimumDelay >= Times[WarnSize - 1];
public void Update() {
for(int i = 0; i < Size - 1; ++i)
Times[i] = Times[i + 1];
Times[Size - 1] = DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
}
}

View file

@ -1,75 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace SharpChat.Reflection {
public class ObjectConstructor<TObject, TAttribute, TDefault>
where TAttribute : ObjectConstructorAttribute
where TDefault : TObject {
private Dictionary<string, Type> Types { get; } = new();
private bool AllowDefault { get; }
public ObjectConstructor(bool allowDefault = true) {
AllowDefault = allowDefault;
Reload();
}
public void Reload() {
Types.Clear();
IEnumerable<Assembly> asms = AppDomain.CurrentDomain.GetAssemblies();
foreach(Assembly asm in asms) {
IEnumerable<Type> types = asm.GetExportedTypes();
foreach(Type type in types) {
Attribute attr = type.GetCustomAttribute(typeof(TAttribute));
if(attr is not null and ObjectConstructorAttribute oca)
Types.Add(oca.Name, type);
}
}
}
public TObject Construct(string name, params object[] args) {
if(name == null)
throw new ArgumentNullException(name);
Type type = !Types.ContainsKey(name)
? (AllowDefault ? typeof(TDefault) : throw new ObjectConstructorObjectNotFoundException(name))
: Types[name];
IEnumerable<object> arguments = args;
IEnumerable<Type> types = arguments.Select(a => a.GetType());
ConstructorInfo[] cis = type.GetConstructors();
ConstructorInfo constructor = null;
for(;;) {
foreach(ConstructorInfo ci in cis) {
IEnumerable<Type> constTypes = ci.GetParameters().Select(p => p.ParameterType);
if(constTypes.Count() != arguments.Count())
continue;
bool isMatch = true;
for(int i = 0; i < constTypes.Count(); ++i)
if(!types.ElementAt(i).IsAssignableTo(constTypes.ElementAt(i))) {
isMatch = false;
break;
}
if(isMatch) {
constructor = ci;
break;
}
}
if(constructor != null || !arguments.Any())
break;
arguments = arguments.Take(arguments.Count() - 1);
types = types.Take(arguments.Count());
}
if(constructor == null)
throw new ObjectConstructorConstructorNotFoundException(name);
return (TObject)constructor.Invoke(arguments.ToArray());
}
}
}

View file

@ -1,12 +0,0 @@
using System;
namespace SharpChat.Reflection {
[AttributeUsage(AttributeTargets.Class)]
public abstract class ObjectConstructorAttribute : Attribute {
public string Name { get; }
public ObjectConstructorAttribute(string name) {
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}

View file

@ -1,16 +0,0 @@
using System;
namespace SharpChat.Reflection {
public class ObjectConstructorException : Exception {
public ObjectConstructorException(string message) : base(message) {
}
}
public class ObjectConstructorObjectNotFoundException : ObjectConstructorException {
public ObjectConstructorObjectNotFoundException(string name) : base($@"Object with name {name} not found.") { }
}
public class ObjectConstructorConstructorNotFoundException : ObjectConstructorException {
public ObjectConstructorConstructorNotFoundException(string name) : base($@"Proper constructor for object {name} not found.") { }
}
}

Some files were not shown because too many files have changed in this diff Show more