Compare commits

..

10 commits

416 changed files with 3952 additions and 15171 deletions

7
.gitignore vendored
View file

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

3
.gitmodules vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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