Compare commits
6 commits
new-master
...
master
Author | SHA1 | Date | |
---|---|---|---|
8ab396668f | |||
3c800dd3e7 | |||
afd8d5b38a | |||
64affb6ef8 | |||
da62de6410 | |||
435635db2d |
490 changed files with 14329 additions and 6483 deletions
|
@ -1,4 +0,0 @@
|
|||
[*.{cs,vb}]
|
||||
|
||||
# IDE0046: Convert to conditional expression
|
||||
dotnet_style_prefer_conditional_expression_over_return = false
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,14 +1,7 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
welcome.txt
|
||||
mariadb.txt
|
||||
login_key.txt
|
||||
http-motd.txt
|
||||
_webdb.txt
|
||||
msz_url.txt
|
||||
sharpchat.cfg
|
||||
SharpChat/version.txt
|
||||
SharpChat.Common/version.txt
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "hamakaze"]
|
||||
path = hamakaze
|
||||
url = https://git.flash.moe/flash/hamakaze.git
|
12
HttpClientTest/HttpClientTest.csproj
Normal file
12
HttpClientTest/HttpClientTest.csproj
Normal file
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\hamakaze\Hamakaze\Hamakaze.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
147
HttpClientTest/Program.cs
Normal file
147
HttpClientTest/Program.cs
Normal file
|
@ -0,0 +1,147 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019-2024 flashwave
|
||||
Copyright (c) 2019-2022 flashwave <me@flash.moe>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
13
MisuzuDataProviderTest/MisuzuDataProviderTest.csproj
Normal file
13
MisuzuDataProviderTest/MisuzuDataProviderTest.csproj
Normal file
|
@ -0,0 +1,13 @@
|
|||
<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>
|
99
MisuzuDataProviderTest/Program.cs
Normal file
99
MisuzuDataProviderTest/Program.cs
Normal file
|
@ -0,0 +1,99 @@
|
|||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
883
Protocol.md
883
Protocol.md
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,6 @@
|
|||
/_/
|
||||
```
|
||||
|
||||
Welcome to the repository of the Flashii Chat server!
|
||||
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).
|
||||
|
||||
The protocol used is based on the protocol found in the original [PHP Sock Chat](https://patchii.net/sockchat/sockchat).
|
||||
A rendered version of the Protocol.md document can be found on [railgun.sh/sockchat](https://railgun.sh/sockchat).
|
||||
> Formerly [PHP Sock Chat](https://github.com/flashwave/mahou-chat/) but without PHP but with C# but also with multiple sessions
|
||||
|
|
344
SharpChat.Common/Bans/BanManager.cs
Normal file
344
SharpChat.Common/Bans/BanManager.cs
Normal file
|
@ -0,0 +1,344 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
14
SharpChat.Common/Bans/IBanClient.cs
Normal file
14
SharpChat.Common/Bans/IBanClient.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
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);
|
||||
}
|
||||
}
|
11
SharpChat.Common/Bans/IBanRecord.cs
Normal file
11
SharpChat.Common/Bans/IBanRecord.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
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; }
|
||||
}
|
||||
}
|
159
SharpChat.Common/Channels/Channel.cs
Normal file
159
SharpChat.Common/Channels/Channel.cs
Normal file
|
@ -0,0 +1,159 @@
|
|||
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}>";
|
||||
}
|
||||
}
|
447
SharpChat.Common/Channels/ChannelManager.cs
Normal file
447
SharpChat.Common/Channels/ChannelManager.cs
Normal file
|
@ -0,0 +1,447 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
421
SharpChat.Common/Channels/ChannelUserRelations.cs
Normal file
421
SharpChat.Common/Channels/ChannelUserRelations.cs
Normal file
|
@ -0,0 +1,421 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
SharpChat.Common/Channels/IChannel.cs
Normal file
18
SharpChat.Common/Channels/IChannel.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
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; }
|
||||
}
|
||||
}
|
11
SharpChat.Common/Channels/IChannelExtensions.cs
Normal file
11
SharpChat.Common/Channels/IChannelExtensions.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
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;
|
||||
}
|
||||
}
|
29
SharpChat.Common/Colour.cs
Normal file
29
SharpChat.Common/Colour.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
45
SharpChat.Common/Configuration/CachedValue.cs
Normal file
45
SharpChat.Common/Configuration/CachedValue.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
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 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) { }
|
||||
public ConfigTypeException(Exception ex) : base(@"Given type does not match the value in the configuration.", ex) { }
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
namespace SharpChat.Configuration {
|
||||
public interface IConfig : IDisposable {
|
||||
/// <summary>
|
||||
/// Creates a proxy object that forces all names to start with the given prefix.
|
||||
|
@ -10,22 +10,22 @@ namespace SharpChat.Config {
|
|||
/// <summary>
|
||||
/// Reads a raw (string) value from the config.
|
||||
/// </summary>
|
||||
string? ReadValue(string name, string? fallback = null);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
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));
|
||||
|
||||
Config = config;
|
||||
Prefix = prefix;
|
||||
|
||||
throw new ArgumentException(@"Prefix must exist.", nameof(prefix));
|
||||
if(Prefix[^1] != ':')
|
||||
Prefix += ':';
|
||||
}
|
||||
|
@ -20,15 +18,15 @@ namespace SharpChat.Config {
|
|||
return Prefix + name;
|
||||
}
|
||||
|
||||
public string? ReadValue(string name, string? fallback = null) {
|
||||
public string ReadValue(string name, string fallback = null) {
|
||||
return Config.ReadValue(GetName(name), fallback);
|
||||
}
|
||||
|
||||
public T? ReadValue<T>(string name, T? fallback = default) {
|
||||
public T ReadValue<T>(string name, T fallback = default) {
|
||||
return Config.ReadValue(GetName(name), fallback);
|
||||
}
|
||||
|
||||
public T? SafeReadValue<T>(string name, T fallback) {
|
||||
public T SafeReadValue<T>(string name, T fallback) {
|
||||
return Config.SafeReadValue(GetName(name), fallback);
|
||||
}
|
||||
|
||||
|
@ -36,7 +34,7 @@ namespace SharpChat.Config {
|
|||
return Config.ScopeTo(GetName(prefix));
|
||||
}
|
||||
|
||||
public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) {
|
||||
public CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null) {
|
||||
return Config.ReadCached(GetName(name), fallback, lifetime);
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ using System.IO;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
namespace SharpChat.Configuration {
|
||||
public class StreamConfig : IConfig {
|
||||
private Stream Stream { get; }
|
||||
private StreamReader StreamReader { get; }
|
||||
|
@ -14,32 +14,32 @@ namespace SharpChat.Config {
|
|||
private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15);
|
||||
|
||||
public StreamConfig(string fileName)
|
||||
: this(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) { }
|
||||
: this(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) {}
|
||||
|
||||
public StreamConfig(Stream stream) {
|
||||
Stream = stream;
|
||||
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
if(!Stream.CanRead)
|
||||
throw new ArgumentException("Provided stream must be readable.", nameof(stream));
|
||||
throw new ArgumentException(@"Provided stream must be readable.", nameof(stream));
|
||||
if(!Stream.CanSeek)
|
||||
throw new ArgumentException("Provided stream must be seekable.", nameof(stream));
|
||||
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) {
|
||||
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;
|
||||
string line;
|
||||
while((line = StreamReader.ReadLine()) != null) {
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
line = line.TrimStart();
|
||||
if(line.StartsWith(";") || line.StartsWith("#"))
|
||||
if(line.StartsWith(@";") || line.StartsWith(@"#"))
|
||||
continue;
|
||||
|
||||
string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
@ -55,16 +55,16 @@ namespace SharpChat.Config {
|
|||
return fallback;
|
||||
}
|
||||
|
||||
public T? ReadValue<T>(string name, T? fallback = default) {
|
||||
object? value = ReadValue(name);
|
||||
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);
|
||||
value = !string.Equals(strVal, @"0", StringComparison.InvariantCultureIgnoreCase)
|
||||
&& !string.Equals(strVal, @"false", StringComparison.InvariantCultureIgnoreCase);
|
||||
else if(type == typeof(string[]))
|
||||
value = strVal.Split(' ');
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ namespace SharpChat.Config {
|
|||
}
|
||||
}
|
||||
|
||||
public T? SafeReadValue<T>(string name, T fallback) {
|
||||
public T SafeReadValue<T>(string name, T fallback) {
|
||||
try {
|
||||
return ReadValue(name, fallback);
|
||||
} catch(ConfigTypeException) {
|
||||
|
@ -87,8 +87,8 @@ namespace SharpChat.Config {
|
|||
public IConfig ScopeTo(string prefix) {
|
||||
return new ScopedConfig(this, prefix);
|
||||
}
|
||||
|
||||
public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) {
|
||||
|
||||
public CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null) {
|
||||
return new CachedValue<T>(this, name, lifetime ?? CACHE_LIFETIME, fallback);
|
||||
}
|
||||
|
145
SharpChat.Common/Context.cs
Normal file
145
SharpChat.Common/Context.cs
Normal file
|
@ -0,0 +1,145 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
8
SharpChat.Common/DataProvider/DataProviderAttribute.cs
Normal file
8
SharpChat.Common/DataProvider/DataProviderAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using SharpChat.Reflection;
|
||||
|
||||
namespace SharpChat.DataProvider {
|
||||
public class DataProviderAttribute : ObjectConstructorAttribute {
|
||||
public DataProviderAttribute(string name) : base(name) {
|
||||
}
|
||||
}
|
||||
}
|
9
SharpChat.Common/DataProvider/IDataProvider.cs
Normal file
9
SharpChat.Common/DataProvider/IDataProvider.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using SharpChat.Bans;
|
||||
using SharpChat.Users.Remote;
|
||||
|
||||
namespace SharpChat.DataProvider {
|
||||
public interface IDataProvider {
|
||||
IBanClient BanClient { get; }
|
||||
IRemoteUserClient UserClient { get; }
|
||||
}
|
||||
}
|
30
SharpChat.Common/DataProvider/Null/NullBanClient.cs
Normal file
30
SharpChat.Common/DataProvider/Null/NullBanClient.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
15
SharpChat.Common/DataProvider/Null/NullDataProvider.cs
Normal file
15
SharpChat.Common/DataProvider/Null/NullDataProvider.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat.Common/DataProvider/Null/NullUserAuthResponse.cs
Normal file
28
SharpChat.Common/DataProvider/Null/NullUserAuthResponse.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
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;
|
||||
}
|
||||
}
|
33
SharpChat.Common/DataProvider/Null/NullUserClient.cs
Normal file
33
SharpChat.Common/DataProvider/Null/NullUserClient.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
81
SharpChat.Common/Database/ADODatabaseReader.cs
Normal file
81
SharpChat.Common/Database/ADODatabaseReader.cs
Normal file
|
@ -0,0 +1,81 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
8
SharpChat.Common/Database/DatabaseBackendAttribute.cs
Normal file
8
SharpChat.Common/Database/DatabaseBackendAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using SharpChat.Reflection;
|
||||
|
||||
namespace SharpChat.Database {
|
||||
public class DatabaseBackendAttribute : ObjectConstructorAttribute {
|
||||
public DatabaseBackendAttribute(string name) : base(name) {
|
||||
}
|
||||
}
|
||||
}
|
7
SharpChat.Common/Database/DatabaseException.cs
Normal file
7
SharpChat.Common/Database/DatabaseException.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Database {
|
||||
public class DatabaseException : Exception {}
|
||||
|
||||
public class InvalidParameterClassTypeException : DatabaseException { }
|
||||
}
|
14
SharpChat.Common/Database/DatabaseType.cs
Normal file
14
SharpChat.Common/Database/DatabaseType.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace SharpChat.Database {
|
||||
public enum DatabaseType {
|
||||
AsciiString,
|
||||
UnicodeString,
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
UInt8,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
}
|
||||
}
|
109
SharpChat.Common/Database/DatabaseWrapper.cs
Normal file
109
SharpChat.Common/Database/DatabaseWrapper.cs
Normal file
|
@ -0,0 +1,109 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
37
SharpChat.Common/Database/IDatabaseBackend.cs
Normal file
37
SharpChat.Common/Database/IDatabaseBackend.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
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; }
|
||||
}
|
||||
}
|
21
SharpChat.Common/Database/IDatabaseCommand.cs
Normal file
21
SharpChat.Common/Database/IDatabaseCommand.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
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();
|
||||
}
|
||||
}
|
7
SharpChat.Common/Database/IDatabaseConnection.cs
Normal file
7
SharpChat.Common/Database/IDatabaseConnection.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Database {
|
||||
public interface IDatabaseConnection : IDisposable {
|
||||
IDatabaseCommand CreateCommand(object query);
|
||||
}
|
||||
}
|
6
SharpChat.Common/Database/IDatabaseParameter.cs
Normal file
6
SharpChat.Common/Database/IDatabaseParameter.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.Database {
|
||||
public interface IDatabaseParameter {
|
||||
string Name { get; }
|
||||
object Value { get; set; }
|
||||
}
|
||||
}
|
37
SharpChat.Common/Database/IDatabaseReader.cs
Normal file
37
SharpChat.Common/Database/IDatabaseReader.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
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);
|
||||
}
|
||||
}
|
63
SharpChat.Common/Database/Null/NullDatabaseBackend.cs
Normal file
63
SharpChat.Common/Database/Null/NullDatabaseBackend.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
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;
|
||||
}
|
||||
}
|
47
SharpChat.Common/Database/Null/NullDatabaseCommand.cs
Normal file
47
SharpChat.Common/Database/Null/NullDatabaseCommand.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
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() {}
|
||||
}
|
||||
}
|
13
SharpChat.Common/Database/Null/NullDatabaseConnection.cs
Normal file
13
SharpChat.Common/Database/Null/NullDatabaseConnection.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
6
SharpChat.Common/Database/Null/NullDatabaseParameter.cs
Normal file
6
SharpChat.Common/Database/Null/NullDatabaseParameter.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.Database.Null {
|
||||
public class NullDatabaseParameter : IDatabaseParameter {
|
||||
public string Name => string.Empty;
|
||||
public object Value { get => null; set { } }
|
||||
}
|
||||
}
|
92
SharpChat.Common/Database/Null/NullDatabaseReader.cs
Normal file
92
SharpChat.Common/Database/Null/NullDatabaseReader.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
15
SharpChat.Common/Events/BroadcastMessageEvent.cs
Normal file
15
SharpChat.Common/Events/BroadcastMessageEvent.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat.Common/Events/ChannelCreateEvent.cs
Normal file
28
SharpChat.Common/Events/ChannelCreateEvent.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
13
SharpChat.Common/Events/ChannelDeleteEvent.cs
Normal file
13
SharpChat.Common/Events/ChannelDeleteEvent.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
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) { }
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/ChannelSessionJoinEvent.cs
Normal file
11
SharpChat.Common/Events/ChannelSessionJoinEvent.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
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) { }
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/ChannelSessionLeaveEvent.cs
Normal file
11
SharpChat.Common/Events/ChannelSessionLeaveEvent.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
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) { }
|
||||
}
|
||||
}
|
47
SharpChat.Common/Events/ChannelUpdateEvent.cs
Normal file
47
SharpChat.Common/Events/ChannelUpdateEvent.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
14
SharpChat.Common/Events/ChannelUserJoinEvent.cs
Normal file
14
SharpChat.Common/Events/ChannelUserJoinEvent.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
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) { }
|
||||
}
|
||||
}
|
17
SharpChat.Common/Events/ChannelUserLeaveEvent.cs
Normal file
17
SharpChat.Common/Events/ChannelUserLeaveEvent.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
83
SharpChat.Common/Events/Event.cs
Normal file
83
SharpChat.Common/Events/Event.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
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}";
|
||||
}
|
||||
}
|
12
SharpChat.Common/Events/EventAttribute.cs
Normal file
12
SharpChat.Common/Events/EventAttribute.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
103
SharpChat.Common/Events/EventDispatcher.cs
Normal file
103
SharpChat.Common/Events/EventDispatcher.cs
Normal file
|
@ -0,0 +1,103 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
12
SharpChat.Common/Events/IEvent.cs
Normal file
12
SharpChat.Common/Events/IEvent.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
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; }
|
||||
}
|
||||
}
|
7
SharpChat.Common/Events/IEventDispatcher.cs
Normal file
7
SharpChat.Common/Events/IEventDispatcher.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace SharpChat.Events {
|
||||
public interface IEventDispatcher {
|
||||
void AddEventHandler(IEventHandler handler);
|
||||
void RemoveEventHandler(IEventHandler handler);
|
||||
void DispatchEvent(object sender, IEvent evt);
|
||||
}
|
||||
}
|
6
SharpChat.Common/Events/IEventExtensions.cs
Normal file
6
SharpChat.Common/Events/IEventExtensions.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.Events {
|
||||
public static class IEventExtensions {
|
||||
public static bool IsBroadcast(this IEvent evt)
|
||||
=> evt.ChannelId == null;
|
||||
}
|
||||
}
|
5
SharpChat.Common/Events/IEventHandler.cs
Normal file
5
SharpChat.Common/Events/IEventHandler.cs
Normal file
|
@ -0,0 +1,5 @@
|
|||
namespace SharpChat.Events {
|
||||
public interface IEventHandler {
|
||||
void HandleEvent(object sender, IEvent evt);
|
||||
}
|
||||
}
|
15
SharpChat.Common/Events/IPBanRemovedEvent.cs
Normal file
15
SharpChat.Common/Events/IPBanRemovedEvent.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
34
SharpChat.Common/Events/MessageCreateEvent.cs
Normal file
34
SharpChat.Common/Events/MessageCreateEvent.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
21
SharpChat.Common/Events/MessageDeleteEvent.cs
Normal file
21
SharpChat.Common/Events/MessageDeleteEvent.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
22
SharpChat.Common/Events/MessageUpdateEvent.cs
Normal file
22
SharpChat.Common/Events/MessageUpdateEvent.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
12
SharpChat.Common/Events/SessionChannelSwitchEvent.cs
Normal file
12
SharpChat.Common/Events/SessionChannelSwitchEvent.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
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) { }
|
||||
}
|
||||
}
|
24
SharpChat.Common/Events/SessionCreatedEvent.cs
Normal file
24
SharpChat.Common/Events/SessionCreatedEvent.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/SessionDestroyEvent.cs
Normal file
11
SharpChat.Common/Events/SessionDestroyEvent.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using SharpChat.Sessions;
|
||||
|
||||
namespace SharpChat.Events {
|
||||
[Event(TYPE)]
|
||||
public class SessionDestroyEvent : Event {
|
||||
public const string TYPE = @"session:destroy";
|
||||
|
||||
public SessionDestroyEvent(ISession session)
|
||||
: base(session) {}
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/SessionPingEvent.cs
Normal file
11
SharpChat.Common/Events/SessionPingEvent.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using SharpChat.Sessions;
|
||||
|
||||
namespace SharpChat.Events {
|
||||
[Event(TYPE)]
|
||||
public class SessionPingEvent : Event {
|
||||
public const string TYPE = @"session:ping";
|
||||
|
||||
public SessionPingEvent(ISession session)
|
||||
: base(session) { }
|
||||
}
|
||||
}
|
29
SharpChat.Common/Events/SessionResumeEvent.cs
Normal file
29
SharpChat.Common/Events/SessionResumeEvent.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/SessionSuspendEvent.cs
Normal file
11
SharpChat.Common/Events/SessionSuspendEvent.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using SharpChat.Sessions;
|
||||
|
||||
namespace SharpChat.Events {
|
||||
[Event(TYPE)]
|
||||
public class SessionSuspendEvent : Event {
|
||||
public const string TYPE = @"session:suspend";
|
||||
|
||||
public SessionSuspendEvent(ISession session)
|
||||
: base(session) { }
|
||||
}
|
||||
}
|
29
SharpChat.Common/Events/UserBanCreatedEvent.cs
Normal file
29
SharpChat.Common/Events/UserBanCreatedEvent.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
14
SharpChat.Common/Events/UserBanRemovedEvent.cs
Normal file
14
SharpChat.Common/Events/UserBanRemovedEvent.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
) { }
|
||||
}
|
||||
}
|
28
SharpChat.Common/Events/UserConnectEvent.cs
Normal file
28
SharpChat.Common/Events/UserConnectEvent.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
16
SharpChat.Common/Events/UserDisconnectEvent.cs
Normal file
16
SharpChat.Common/Events/UserDisconnectEvent.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
85
SharpChat.Common/Events/UserUpdateEvent.cs
Normal file
85
SharpChat.Common/Events/UserUpdateEvent.cs
Normal file
|
@ -0,0 +1,85 @@
|
|||
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
|
||||
) { }
|
||||
}
|
||||
}
|
43
SharpChat.Common/Logger.cs
Normal file
43
SharpChat.Common/Logger.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpChat {
|
||||
public static class Logger {
|
||||
public static void Write()
|
||||
=> Console.WriteLine();
|
||||
|
||||
[Conditional(@"DEBUG")]
|
||||
public static void Debug()
|
||||
=> Write();
|
||||
|
||||
public static void Write(string str)
|
||||
=> Console.WriteLine(string.Format(@"[{1}] {0}", str, DateTime.Now));
|
||||
|
||||
[Conditional(@"DEBUG")]
|
||||
public static void Debug(string str)
|
||||
=> Write(str);
|
||||
|
||||
public static void Write(byte[] bytes)
|
||||
=> Write(Encoding.UTF8.GetString(bytes));
|
||||
|
||||
[Conditional(@"DEBUG")]
|
||||
public static void Debug(byte[] bytes)
|
||||
=> Write(bytes);
|
||||
|
||||
public static void Write(object obj)
|
||||
=> Write(obj?.ToString() ?? string.Empty);
|
||||
|
||||
[Conditional(@"DEBUG")]
|
||||
public static void Debug(object obj)
|
||||
=> Write(obj);
|
||||
|
||||
public static void Write(IEnumerable<object> objs)
|
||||
=> Write(string.Join(@", ", objs));
|
||||
|
||||
[Conditional(@"DEBUG")]
|
||||
public static void Debug(IEnumerable<object> objs)
|
||||
=> Write(objs);
|
||||
}
|
||||
}
|
16
SharpChat.Common/Messages/IMessage.cs
Normal file
16
SharpChat.Common/Messages/IMessage.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
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; }
|
||||
}
|
||||
}
|
10
SharpChat.Common/Messages/IMessageExtensions.cs
Normal file
10
SharpChat.Common/Messages/IMessageExtensions.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace SharpChat.Messages {
|
||||
public static class IMessageExtensions {
|
||||
public static string GetSanitisedText(this IMessage msg)
|
||||
=> msg.Text
|
||||
.Replace(@"<", @"<")
|
||||
.Replace(@">", @">")
|
||||
.Replace("\n", @" <br/> ")
|
||||
.Replace("\t", @" ");
|
||||
}
|
||||
}
|
34
SharpChat.Common/Messages/Message.cs
Normal file
34
SharpChat.Common/Messages/Message.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
99
SharpChat.Common/Messages/MessageManager.cs
Normal file
99
SharpChat.Common/Messages/MessageManager.cs
Normal file
|
@ -0,0 +1,99 @@
|
|||
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);
|
||||
}
|
||||
}
|
33
SharpChat.Common/Messages/Storage/ADOMessage.cs
Normal file
33
SharpChat.Common/Messages/Storage/ADOMessage.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
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"));
|
||||
}
|
||||
}
|
||||
}
|
31
SharpChat.Common/Messages/Storage/ADOMessageChannel.cs
Normal file
31
SharpChat.Common/Messages/Storage/ADOMessageChannel.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
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}>";
|
||||
}
|
||||
}
|
137
SharpChat.Common/Messages/Storage/ADOMessageStorage.cs
Normal file
137
SharpChat.Common/Messages/Storage/ADOMessageStorage.cs
Normal file
|
@ -0,0 +1,137 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
33
SharpChat.Common/Messages/Storage/ADOMessageUser.cs
Normal file
33
SharpChat.Common/Messages/Storage/ADOMessageUser.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
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}>";
|
||||
}
|
||||
}
|
11
SharpChat.Common/Messages/Storage/IMessageStorage.cs
Normal file
11
SharpChat.Common/Messages/Storage/IMessageStorage.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
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);
|
||||
}
|
||||
}
|
42
SharpChat.Common/Messages/Storage/MemoryMessage.cs
Normal file
42
SharpChat.Common/Messages/Storage/MemoryMessage.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat.Common/Messages/Storage/MemoryMessageChannel.cs
Normal file
28
SharpChat.Common/Messages/Storage/MemoryMessageChannel.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
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}>";
|
||||
}
|
||||
}
|
90
SharpChat.Common/Messages/Storage/MemoryMessageStorage.cs
Normal file
90
SharpChat.Common/Messages/Storage/MemoryMessageStorage.cs
Normal file
|
@ -0,0 +1,90 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
SharpChat.Common/Messages/Storage/MemoryMessageUser.cs
Normal file
30
SharpChat.Common/Messages/Storage/MemoryMessageUser.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
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}>";
|
||||
}
|
||||
}
|
170
SharpChat.Common/Protocol/ConnectionList.cs
Normal file
170
SharpChat.Common/Protocol/ConnectionList.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
15
SharpChat.Common/Protocol/IConnection.cs
Normal file
15
SharpChat.Common/Protocol/IConnection.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
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();
|
||||
}
|
||||
}
|
9
SharpChat.Common/Protocol/IServer.cs
Normal file
9
SharpChat.Common/Protocol/IServer.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using SharpChat.Events;
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace SharpChat.Protocol {
|
||||
public interface IServer : IEventHandler, IDisposable {
|
||||
void Listen(EndPoint endPoint);
|
||||
}
|
||||
}
|
16
SharpChat.Common/Protocol/Null/NullServer.cs
Normal file
16
SharpChat.Common/Protocol/Null/NullServer.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
11
SharpChat.Common/Protocol/ProtocolException.cs
Normal file
11
SharpChat.Common/Protocol/ProtocolException.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
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.") { }
|
||||
}
|
||||
}
|
8
SharpChat.Common/Protocol/ServerAttribute.cs
Normal file
8
SharpChat.Common/Protocol/ServerAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using SharpChat.Reflection;
|
||||
|
||||
namespace SharpChat.Protocol {
|
||||
public class ServerAttribute : ObjectConstructorAttribute {
|
||||
public ServerAttribute(string name) : base(name) {
|
||||
}
|
||||
}
|
||||
}
|
42
SharpChat.Common/RNG.cs
Normal file
42
SharpChat.Common/RNG.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpChat {
|
||||
public static class RNG {
|
||||
private static object Lock { get; } = new();
|
||||
private static Random NormalRandom { get; } = new();
|
||||
private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create();
|
||||
|
||||
public static int Next() {
|
||||
lock (Lock)
|
||||
return NormalRandom.Next();
|
||||
}
|
||||
|
||||
public static int Next(int max) {
|
||||
lock (Lock)
|
||||
return NormalRandom.Next(max);
|
||||
}
|
||||
|
||||
public static int Next(int min, int max) {
|
||||
lock (Lock)
|
||||
return NormalRandom.Next(min, max);
|
||||
}
|
||||
|
||||
public static void NextBytes(byte[] buffer) {
|
||||
lock(Lock)
|
||||
SecureRandom.GetBytes(buffer);
|
||||
}
|
||||
|
||||
public const string ID_CHARS = @"abcdefghijklmnopqrstuvwxyz0123456789-_ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
public static string NextString(int length, string chars = ID_CHARS) {
|
||||
byte[] buffer = new byte[length];
|
||||
NextBytes(buffer);
|
||||
StringBuilder sb = new();
|
||||
foreach(byte b in buffer)
|
||||
sb.Append(chars[b % chars.Length]);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
80
SharpChat.Common/RateLimiting/RateLimitManager.cs
Normal file
80
SharpChat.Common/RateLimiting/RateLimitManager.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
SharpChat.Common/RateLimiting/RateLimiter.cs
Normal file
38
SharpChat.Common/RateLimiting/RateLimiter.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue