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;
///
/// Idempotent server setup. Runs once at boot from Program.cs.
///
/// Each "Ensure*" helper either inserts a missing row or patches an existing one so the
/// declared state matches the code. Running this twice in a row is a no-op.
///
/// What it provisions:
/// - Verifies the three test users exist via CoreClientService (currently a hardcoded stub).
/// - Creates the "Test Server" row in the servers table if missing.
/// - Adds those users to server_members, with Keeper317 as IsOwner=true.
/// - Creates the four premade channels with correct ChannelType and IsReadOnly flags:
/// welcome (Text, read-only) general (Text)
/// files (File, read-only) voice-general (Voice)
/// - Links #general → #files so attachments posted in #general auto-mirror to #files.
/// - Creates the three roles: Admin (all perms), Moderator (manage messages), Member (read+send).
/// - Assigns exactly one role per user (Keeper→Admin, Kira→Moderator, Test→Member).
/// SetUserRoleAsync DELETES stale assignments to guarantee single-role-per-user.
/// - Writes channel_permissions overrides explicitly denying Members SendMessages in
/// #welcome and #files.
/// - Generates the server's RSA keypair + the channel AES key on first boot, stores both
/// in server_encryption_keys, and copies them into ChatSocketBehavior's static fields.
///
public sealed class ServerBootstrapService
{
private readonly SurrealDbClient _db;
private readonly CoreClientService _coreClient;
private readonly ChannelCryptoService _cryptoService;
public ServerBootstrapService(
SurrealDbClient db,
CoreClientService coreClient,
ChannelCryptoService cryptoService)
{
_db = db;
_coreClient = coreClient;
_cryptoService = cryptoService;
}
public async Task InitializeAsync()
{
var keeper = await _coreClient.GetUserByUsernameAsync("Keeper317");
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.");
if (!keeper.Licensed || !kira.Licensed || !test.Licensed)
throw new InvalidOperationException("One or more required users are not licensed.");
Console.WriteLine($"Core verified: {keeper.Username}, {kira.Username}, {test.Username}");
var server = await GetServerByNameAsync("Test Server");
if (server is null)
{
server = await _db.Create("servers", new Servers
{
Name = "Test Server",
OwnerUserId = keeper.Id,
CreatedAt = DateTime.UtcNow
});
Console.WriteLine($"Server created: {ToJson(server)}");
}
else
{
Console.WriteLine($"Server already exists: {server.Name}");
}
await EnsureServerMemberAsync(keeper.Id, isOwner: true);
await EnsureServerMemberAsync(kira.Id, isOwner: false);
await EnsureServerMemberAsync(test.Id, isOwner: false);
Console.WriteLine("Server members ensured.");
var tBase = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
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 serverKeys = E2EeHelper.GenerateRsaKeyPair();
existingKey = await _db.Create("server_encryption_keys", new ServerEncryptionKeys
{
KeyBase64 = keyBase64,
PublicKey = serverKeys.publicKey,
PrivateKey = serverKeys.privateKey,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
});
Console.WriteLine("Server encryption key created.");
}
else
{
Console.WriteLine("Server encryption key already exists.");
}
ChatSocketBehavior.ServerPublicKey = existingKey.PublicKey;
ChatSocketBehavior.ServerPrivateKey = existingKey.PrivateKey;
ChatSocketBehavior.ChannelDbKey = existingKey.KeyBase64;
}
private async Task EnsureServerMemberAsync(string userId, bool isOwner)
{
var members = await _db.Select("server_members");
var existing = members.FirstOrDefault(m => m.UserId == userId);
if (existing is not null)
{
if (existing.IsOwner != isOwner)
{
existing.IsOwner = isOwner;
await _db.Merge(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 async Task EnsureChannelAsync(
string name, ChannelType type, string group, bool isReadOnly, DateTime createdAt)
{
var channels = await _db.Select("channels");
var existing = channels.FirstOrDefault(c => c.Name == name);
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(existing);
Console.WriteLine($"Channel updated: {name}");
}
else
{
Console.WriteLine($"Channel already correct: {name}");
}
return existing;
}
var channel = await _db.Create("channels", new Channels
{
Name = name,
Type = type,
Group = group,
IsReadOnly = isReadOnly,
CreatedAt = createdAt
});
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(channel);
Console.WriteLine($"File link set: {channel.Name} → {fileChannelId}");
}
private async Task EnsureRoleAsync(string name, PermissionFlags permissions, int priority)
{
var roles = await _db.Select("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("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("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 GetServerByNameAsync(string name)
{
var servers = await _db.Select("servers");
return servers.FirstOrDefault(x => x.Name == name);
}
private async Task GetLatestServerEncryptionKeyAsync()
{
var keys = await _db.Select("server_encryption_keys");
return keys.OrderByDescending(x => x.CreatedAt).FirstOrDefault();
}
private static string GetRecordId(object? id)
{
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 static string ToJson(object? obj) =>
JsonSerializer.Serialize(obj, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}