Update: Text Channel Stuff

Bugs: Files don't work
Bugs: Video In-Line don't work

Added: idk, everything?
This commit is contained in:
2026-06-03 13:19:21 -04:00
parent cd2d809322
commit f819d7284e
20 changed files with 3447 additions and 908 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,13 @@ using System.Text.Json;
using RelayServer.Models;
using RelayServer.Services.Chat;
using RelayServer.Services.Crypto;
using RelayShared.Services;
using SurrealDb.Net;
namespace RelayServer.Services.Core;
public sealed class ServerBootstrapService
{
// TODO: Make channels dynamically addable
// TODO: Add logic for channel types (ENUM)
// TODO: Add logic for channel groups for future UI use
private readonly SurrealDbClient _db;
private readonly CoreClientService _coreClient;
private readonly ChannelCryptoService _cryptoService;
@@ -29,8 +26,8 @@ public sealed class ServerBootstrapService
public async Task InitializeAsync()
{
var keeper = await _coreClient.GetUserByUsernameAsync("Keeper317");
var kira = await _coreClient.GetUserByUsernameAsync("Ru_Kira");
var test = await _coreClient.GetUserByUsernameAsync("Test");
var kira = await _coreClient.GetUserByUsernameAsync("Ru_Kira");
var test = await _coreClient.GetUserByUsernameAsync("Test");
if (keeper is null || kira is null || test is null)
throw new InvalidOperationException("One or more required users do not exist in RelayCore.");
@@ -38,9 +35,7 @@ public sealed class ServerBootstrapService
if (!keeper.Licensed || !kira.Licensed || !test.Licensed)
throw new InvalidOperationException("One or more required users are not licensed.");
Console.WriteLine($"Core verified user: {keeper.Username}");
Console.WriteLine($"Core verified user: {kira.Username}");
Console.WriteLine($"Core verified user: {test.Username}");
Console.WriteLine($"Core verified: {keeper.Username}, {kira.Username}, {test.Username}");
var server = await GetServerByNameAsync("Test Server");
@@ -52,44 +47,61 @@ public sealed class ServerBootstrapService
OwnerUserId = keeper.Id,
CreatedAt = DateTime.UtcNow
});
Console.WriteLine($"Server created: {ToJsonString(server)}");
Console.WriteLine($"Server created: {ToJson(server)}");
}
else
{
Console.WriteLine($"Server already exists: {ToJsonString(server)}");
Console.WriteLine($"Server already exists: {server.Name}");
}
await EnsureServerMemberAsync(keeper.Id, true);
await EnsureServerMemberAsync(kira.Id, false);
await EnsureServerMemberAsync(test.Id, false);
await EnsureServerMemberAsync(keeper.Id, isOwner: true);
await EnsureServerMemberAsync(kira.Id, isOwner: false);
await EnsureServerMemberAsync(test.Id, isOwner: false);
Console.WriteLine("Server members ensured.");
var channel = await EnsureChannelAsync("general", DateTime.UtcNow);
var channel2 = await EnsureChannelAsync("files", DateTime.UtcNow.Subtract(new TimeSpan(0, 4, 0, 0)));
var channel3 = await EnsureChannelAsync("welcome", DateTime.UtcNow.Subtract(new TimeSpan(1, 4, 4, 4)));
var channel4 = await EnsureChannelAsync("voice-general", DateTime.UtcNow.Subtract(new TimeSpan(0, 2, 0, 0)));
var tBase = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
Console.WriteLine($"Resolved channelId: {GetRecordId(channel.Id)}");
Console.WriteLine($"Resolved channelId: {GetRecordId(channel2.Id)}");
Console.WriteLine($"Resolved channelId: {GetRecordId(channel3.Id)}");
Console.WriteLine($"Resolved channelId: {GetRecordId(channel4.Id)}");
var chWelcome = await EnsureChannelAsync("welcome", ChannelType.Text, group: "General", isReadOnly: true, createdAt: tBase);
var chGeneral = await EnsureChannelAsync("general", ChannelType.Text, group: "General", isReadOnly: false, createdAt: tBase.AddHours(1));
var chFiles = await EnsureChannelAsync("files", ChannelType.File, group: "General", isReadOnly: true, createdAt: tBase.AddHours(2));
var chVoice = await EnsureChannelAsync("voice-general", ChannelType.Voice, group: "General", isReadOnly: false, createdAt: tBase.AddHours(3));
Console.WriteLine($"Channels: {GetRecordId(chWelcome.Id)} | {GetRecordId(chGeneral.Id)} | {GetRecordId(chFiles.Id)} | {GetRecordId(chVoice.Id)}");
await EnsureFileChannelLinkAsync(chGeneral, GetRecordId(chFiles.Id));
var adminRole = await EnsureRoleAsync("Admin", PermissionFlags.Administrator, priority: 0);
var modRole = await EnsureRoleAsync("Moderator", PermissionFlags.ReadMessages | PermissionFlags.SendMessages | PermissionFlags.ManageMessages, priority: 1);
var memberRole = await EnsureRoleAsync("Member", PermissionFlags.ReadMessages | PermissionFlags.SendMessages, priority: 2);
Console.WriteLine($"Roles ensured: Admin={GetRecordId(adminRole.Id)}, Mod={GetRecordId(modRole.Id)}, Member={GetRecordId(memberRole.Id)}");
await SetUserRoleAsync(keeper.Id, GetRecordId(adminRole.Id));
await SetUserRoleAsync(kira.Id, GetRecordId(modRole.Id));
await SetUserRoleAsync(test.Id, GetRecordId(memberRole.Id));
Console.WriteLine("User roles set.");
await EnsureChannelPermissionAsync(GetRecordId(chWelcome.Id), GetRecordId(memberRole.Id),
allow: PermissionFlags.ReadMessages, deny: PermissionFlags.SendMessages);
await EnsureChannelPermissionAsync(GetRecordId(chFiles.Id), GetRecordId(memberRole.Id),
allow: PermissionFlags.ReadMessages, deny: PermissionFlags.SendMessages);
Console.WriteLine("Channel permissions ensured.");
var existingKey = await GetLatestServerEncryptionKeyAsync();
if (existingKey is null)
{
var keyBase64 = _cryptoService.GenerateKey();
var keyBase64 = _cryptoService.GenerateKey();
var serverKeys = E2EeHelper.GenerateRsaKeyPair();
existingKey = await _db.Create("server_encryption_keys", new ServerEncryptionKeys
{
KeyBase64 = keyBase64,
PublicKey = serverKeys.publicKey,
KeyBase64 = keyBase64,
PublicKey = serverKeys.publicKey,
PrivateKey = serverKeys.privateKey,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
});
Console.WriteLine("Server encryption key created.");
@@ -104,92 +116,181 @@ public sealed class ServerBootstrapService
ChatSocketBehavior.ChannelDbKey = existingKey.KeyBase64;
}
private static string ToJsonString(object? obj)
private async Task EnsureServerMemberAsync(string userId, bool isOwner)
{
return JsonSerializer.Serialize(obj, new JsonSerializerOptions
var members = await _db.Select<ServerMembers>("server_members");
var existing = members.FirstOrDefault(m => m.UserId == userId);
if (existing is not null)
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
if (existing.IsOwner != isOwner)
{
existing.IsOwner = isOwner;
await _db.Merge<ServerMembers, ServerMembers>(existing);
Console.WriteLine($"Member IsOwner updated: {userId} → {isOwner}");
}
else
{
Console.WriteLine($"Member already correct: {userId}");
}
return;
}
await _db.Create("server_members", new ServerMembers
{
UserId = userId,
JoinedAt = DateTime.UtcNow,
IsOwner = isOwner
});
Console.WriteLine($"Member created: {userId} (IsOwner={isOwner})");
}
private static string GetRecordId(object? id)
private async Task<Channels> EnsureChannelAsync(
string name, ChannelType type, string group, bool isReadOnly, DateTime createdAt)
{
if (id is null)
return string.Empty;
var channels = await _db.Select<Channels>("channels");
var existing = channels.FirstOrDefault(c => c.Name == name);
var json = JsonSerializer.Serialize(id);
if (existing is not null)
{
bool dirty = existing.Type != type || existing.Group != group || existing.IsReadOnly != isReadOnly;
if (dirty)
{
existing.Type = type;
existing.Group = group;
existing.IsReadOnly = isReadOnly;
await _db.Merge<Channels, Channels>(existing);
Console.WriteLine($"Channel updated: {name}");
}
else
{
Console.WriteLine($"Channel already correct: {name}");
}
return existing;
}
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var channel = await _db.Create("channels", new Channels
{
Name = name,
Type = type,
Group = group,
IsReadOnly = isReadOnly,
CreatedAt = createdAt
});
var recordId = root.GetProperty("Id").GetString() ?? string.Empty;
var table = root.GetProperty("Table").GetString() ?? string.Empty;
return $"{table}:{recordId}";
Console.WriteLine($"Channel created: {name} ({type})");
return channel;
}
private async Task EnsureFileChannelLinkAsync(Channels channel, string fileChannelId)
{
if (channel.LinkedFileChannelId == fileChannelId)
{
Console.WriteLine($"File link already correct: {channel.Name} → {fileChannelId}");
return;
}
channel.LinkedFileChannelId = fileChannelId;
await _db.Merge<Channels, Channels>(channel);
Console.WriteLine($"File link set: {channel.Name} → {fileChannelId}");
}
private async Task<Roles> EnsureRoleAsync(string name, PermissionFlags permissions, int priority)
{
var roles = await _db.Select<Roles>("roles");
var existing = roles.FirstOrDefault(r => r.Name == name);
if (existing is not null)
{
Console.WriteLine($"Role already exists: {name}");
return existing;
}
var role = await _db.Create("roles", new Roles
{
Name = name,
Permissions = permissions,
Priority = priority,
CreatedAt = DateTime.UtcNow
});
Console.WriteLine($"Role created: {name}");
return role;
}
private async Task SetUserRoleAsync(string userId, string roleId)
{
var userRoles = await _db.Select<UserRoles>("user_roles");
var existing = userRoles
.Where(ur => string.Equals(ur.UserId, userId, StringComparison.OrdinalIgnoreCase))
.ToList();
bool alreadyCorrect = existing.Count == 1 && existing[0].RoleId == roleId;
if (alreadyCorrect)
{
Console.WriteLine($"UserRole already correct: {userId} → {roleId}");
return;
}
foreach (var stale in existing)
{
if (stale.Id is not null)
await _db.Delete(stale.Id);
}
await _db.Create("user_roles", new UserRoles
{
UserId = userId,
RoleId = roleId,
AssignedAt = DateTime.UtcNow
});
Console.WriteLine($"UserRole set: {userId} → {roleId}");
}
private async Task EnsureChannelPermissionAsync(
string channelId, string roleId, PermissionFlags allow, PermissionFlags deny)
{
var perms = await _db.Select<ChannelPermissions>("channel_permissions");
if (perms.Any(cp => cp.ChannelId == channelId && cp.RoleId == roleId))
{
Console.WriteLine($"ChannelPermission already exists: {channelId} → {roleId}");
return;
}
await _db.Create("channel_permissions", new ChannelPermissions
{
ChannelId = channelId,
RoleId = roleId,
Allow = allow,
Deny = deny
});
Console.WriteLine($"ChannelPermission created: {channelId} → {roleId} | allow={allow}, deny={deny}");
}
private async Task<Servers?> GetServerByNameAsync(string name)
{
var servers = await _db.Select<Servers>("servers");
return servers.FirstOrDefault(x => x.Name == name);
}
private async Task<ServerMembers?> GetServerMemberByUserIdAsync(string userId)
{
var members = await _db.Select<ServerMembers>("server_members");
return members.FirstOrDefault(x => x.UserId == userId);
}
private async Task<Channels?> GetChannelByNameAsync(string name)
{
var channels = await _db.Select<Channels>("channels");
return channels.FirstOrDefault(x => x.Name == name);
}
private async Task<ServerEncryptionKeys?> GetLatestServerEncryptionKeyAsync()
{
var keys = await _db.Select<ServerEncryptionKeys>("server_encryption_keys");
return keys
.OrderByDescending(x => x.CreatedAt)
.FirstOrDefault();
return keys.OrderByDescending(x => x.CreatedAt).FirstOrDefault();
}
private async Task EnsureServerMemberAsync(string userId, bool isOwner)
private static string GetRecordId(object? id)
{
var existing = await GetServerMemberByUserIdAsync(userId);
if (existing is not null)
{
Console.WriteLine($"Server member already exists for {userId}");
return;
}
await _db.Create("server_members", new ServerMembers
{
UserId = userId,
JoinedAt = DateTime.UtcNow,
IsOwner = isOwner
});
Console.WriteLine($"Server member created for {userId}");
if (id is null) return string.Empty;
var json = JsonSerializer.Serialize(id);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
return $"{root.GetProperty("Table").GetString()}:{root.GetProperty("Id").GetString()}";
}
private async Task<Channels> EnsureChannelAsync(string name, DateTime createdAt)
{
var existing = await GetChannelByNameAsync(name);
if (existing is not null)
{
Console.WriteLine($"Channel already exists: {name}");
return existing;
}
var channel = await _db.Create("channels", new Channels
private static string ToJson(object? obj) =>
JsonSerializer.Serialize(obj, new JsonSerializerOptions
{
Name = name,
CreatedAt = createdAt
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
Console.WriteLine($"Channel created: {ToJsonString(channel)}");
return channel;
}
}
}

View File

@@ -0,0 +1,160 @@
using RelayServer.Models;
using SurrealDb.Net;
namespace RelayServer.Services.Data;
public sealed class PermissionService
{
private readonly SurrealDbClient _db;
public PermissionService(SurrealDbClient db)
{
_db = db;
}
public async Task<bool> CanSendMessagesAsync(string username, string channelId)
{
if (await IsOwnerOrAdminAsync(username))
return true;
if (await IsChannelReadOnlyAsync(channelId))
return false;
return await HasPermissionAsync(username, channelId, PermissionFlags.SendMessages);
}
public async Task<bool> CanManageChannelsAsync(string username) =>
await IsOwnerOrAdminAsync(username) ||
await HasGlobalPermissionAsync(username, PermissionFlags.ManageChannels);
public async Task<bool> CanManageMessagesAsync(string username, string channelId) =>
await IsOwnerOrAdminAsync(username) ||
await HasPermissionAsync(username, channelId, PermissionFlags.ManageMessages);
public async Task<bool> IsAdministratorAsync(string username) =>
await IsOwnerOrAdminAsync(username);
public async Task<bool> CanViewChannelAsync(string username, string channelId)
{
if (await IsOwnerOrAdminAsync(username)) return true;
return !await IsDeniedByChannelAsync(username, channelId, PermissionFlags.ViewChannel);
}
public async Task<bool> CanSpeakAsync(string username, string channelId)
{
if (await IsOwnerOrAdminAsync(username)) return true;
return !await IsDeniedByChannelAsync(username, channelId, PermissionFlags.Speak);
}
public async Task<bool> CanDeleteChannelAsync(string username) =>
await IsOwnerOrAdminAsync(username) ||
await HasGlobalPermissionAsync(username, PermissionFlags.ManageChannels) ||
await HasGlobalPermissionAsync(username, PermissionFlags.DeleteChannel);
public async Task<bool> CanEditChannelAsync(string username) =>
await IsOwnerOrAdminAsync(username) ||
await HasGlobalPermissionAsync(username, PermissionFlags.ManageChannels) ||
await HasGlobalPermissionAsync(username, PermissionFlags.EditChannel);
private async Task<bool> IsOwnerOrAdminAsync(string username)
{
if (await IsServerOwnerAsync(username))
return true;
var roles = await GetUserRolesAsync(username);
return roles.Any(r => r.Permissions.HasFlag(PermissionFlags.Administrator));
}
private async Task<bool> HasPermissionAsync(
string username, string channelId, PermissionFlags flag)
{
if (await IsOwnerOrAdminAsync(username))
return true;
var userRoles = await GetUserRolesAsync(username);
if (userRoles.Count == 0) return false;
var channelOverrides = await GetChannelPermissionsAsync(channelId);
var userRoleIds = new HashSet<string>(userRoles.Select(r => GetRecordIdString(r.Id)));
foreach (var co in channelOverrides.Where(co => userRoleIds.Contains(co.RoleId)))
if (co.Deny.HasFlag(flag)) return false;
foreach (var co in channelOverrides.Where(co => userRoleIds.Contains(co.RoleId)))
if (co.Allow.HasFlag(flag)) return true;
return userRoles.Any(r => r.Permissions.HasFlag(flag));
}
private async Task<bool> HasGlobalPermissionAsync(string username, PermissionFlags flag)
{
var roles = await GetUserRolesAsync(username);
return roles.Any(r =>
r.Permissions.HasFlag(PermissionFlags.Administrator) ||
r.Permissions.HasFlag(flag));
}
private async Task<bool> IsDeniedByChannelAsync(string username, string channelId, PermissionFlags flag)
{
var userRoles = await GetUserRolesAsync(username);
if (userRoles.Count == 0) return false;
var channelOverrides = await GetChannelPermissionsAsync(channelId);
var userRoleIds = new HashSet<string>(userRoles.Select(r => GetRecordIdString(r.Id)));
return channelOverrides
.Where(co => userRoleIds.Contains(co.RoleId))
.Any(co => co.Deny.HasFlag(flag));
}
private async Task<bool> IsServerOwnerAsync(string username)
{
var userId = $"users:{username.ToLower()}";
var members = await _db.Select<ServerMembers>("server_members");
return members.Any(m =>
string.Equals(m.UserId, userId, StringComparison.OrdinalIgnoreCase) &&
m.IsOwner);
}
private async Task<List<Roles>> GetUserRolesAsync(string username)
{
var userId = $"users:{username.ToLower()}";
var userRoleLinks = await _db.Select<UserRoles>("user_roles");
var userRoleIds = userRoleLinks
.Where(ur => string.Equals(ur.UserId, userId, StringComparison.OrdinalIgnoreCase))
.Select(ur => ur.RoleId)
.ToHashSet();
if (userRoleIds.Count == 0) return [];
var allRoles = await _db.Select<Roles>("roles");
return allRoles
.Where(r => userRoleIds.Contains(GetRecordIdString(r.Id)))
.ToList();
}
private async Task<List<ChannelPermissions>> GetChannelPermissionsAsync(string channelId)
{
var all = await _db.Select<ChannelPermissions>("channel_permissions");
return all.Where(cp => cp.ChannelId == channelId).ToList();
}
private async Task<bool> IsChannelReadOnlyAsync(string channelId)
{
var channels = await _db.Select<Channels>("channels");
var channel = channels.FirstOrDefault(c => GetRecordIdString(c.Id) == channelId);
return channel?.IsReadOnly ?? false;
}
private static string GetRecordIdString(object? id)
{
if (id is null) return string.Empty;
var json = System.Text.Json.JsonSerializer.Serialize(id);
using var doc = System.Text.Json.JsonDocument.Parse(json);
var root = doc.RootElement;
var recordId = root.GetProperty("Id").GetString() ?? string.Empty;
var table = root.GetProperty("Table").GetString() ?? string.Empty;
return $"{table}:{recordId}";
}
}