566 lines
No EOL
22 KiB
C#
566 lines
No EOL
22 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using LarpixServer.Filesystem;
|
|
using LarpixServer.Utils.Jsons;
|
|
|
|
namespace LarpixServer.Utils;
|
|
|
|
public class Utils
|
|
{
|
|
public static string SERVER_KEY = "SecretThingOMGSoHardToGuess";
|
|
|
|
public static string DOMAIN = "example.com";
|
|
|
|
public static string OWNER_CONTACT = "larpix://@admin:example.com";
|
|
|
|
public static int PORT = 8090;
|
|
public static ConcurrentBag<string> VOICE_IPS = new();
|
|
|
|
// Shared with LarpixVoice: HMAC key used to sign voice-join tickets.
|
|
public static string VOICE_KEY = "";
|
|
|
|
public sealed record VoiceTicketRecord(byte[] ConnKey, byte[] Proof, string RoomId, string UserId, long Exp);
|
|
|
|
// Outstanding (issued, not yet consumed) voice tickets: publicId -> record.
|
|
// Consumed atomically by /_larpix/voice/auth; sweeper removes expired ones.
|
|
public static ConcurrentDictionary<string, VoiceTicketRecord> VoiceTickets = new();
|
|
|
|
// Per-IP rate limit state for the internal voice auth endpoint.
|
|
// Fixed 60s window: (request count, window reset unix time).
|
|
public static ConcurrentDictionary<string, (int count, long resetAt)> VoiceAuthLimits = new();
|
|
|
|
// Rate limity per-minute (okno 60s). Wszystkie konfigurowalne przez .env.
|
|
public static ConcurrentDictionary<string, (int count, long resetAt)> AuthRateLimits = new();
|
|
public static ConcurrentDictionary<string, (int count, long resetAt)> NonceRateLimits = new();
|
|
public static ConcurrentDictionary<string, (int count, long resetAt)> KdfSaltRateLimits = new();
|
|
public static ConcurrentDictionary<string, (int count, long resetAt)> AccountInitRateLimits = new();
|
|
|
|
public static int AUTH_RATE_LIMIT = 10; // próby logowania /auth + /ws na konto+IP
|
|
public static int NONCE_RATE_LIMIT = 240; // pobrania nonce per konto
|
|
public static int KDFSALT_RATE_LIMIT = 60; // pobrania soli per IP
|
|
public static int ACCOUNT_INIT_RATE_LIMIT = 10; // inicjalizacje rejestracji (KEM keygen) per IP
|
|
|
|
// Zwraca true jeśli przekroczono limit. Okno stałe 60s.
|
|
public static bool RateLimited(ConcurrentDictionary<string, (int count, long resetAt)> store, string key, int limit)
|
|
{
|
|
if (limit <= 0) return false; // <=0 wyłącza limit
|
|
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
var window = store.AddOrUpdate(key,
|
|
(1, nowSec + 60),
|
|
(_, old) => old.resetAt <= nowSec ? (1, nowSec + 60) : (old.count + 1, old.resetAt));
|
|
return window.count > limit;
|
|
}
|
|
|
|
public static string DATA_DIR = "./data";
|
|
public static string ROOMS_DIR = "./data/rooms";
|
|
public static string ACCOUNTS_DIR = "./data/accounts";
|
|
|
|
public static string ACCOUNTS_DATA_DIR = "./data/accounts/data";
|
|
public static string ACCOUNTS_NAME_DIR = "./data/accounts/name";
|
|
public static string ACCOUNTS_FREEID_DIR = "./data/accounts/freeid";
|
|
|
|
public static int CACHE_ENTRY_SIZE = 10240;
|
|
public static int FILE_CACHE_SIZE = 800000;
|
|
public static int EXIST_CACHE_SIZE = 1000000;
|
|
public static int DIR_CACHE_SIZE = 800000;
|
|
public static int LOCK_SIZE = 1000000;
|
|
|
|
public static int MAX_BODY_SIZE = 20971520; //20mb //2621440; //2.5mb
|
|
public static int ACTION_RATE_LIMIT = 120;
|
|
|
|
// All rate limits below are requests per minute (fixed 60s window).
|
|
public static int VOICE_AUTH_RATE_LIMIT = 3600;
|
|
|
|
public static int PRIVATE_STORAGE_LIMIT = 5242880;
|
|
public static int PUBLIC_STORAGE_LIMIT = 10485760;
|
|
|
|
public static int ATTACHMENT_SIZE_LIMIT = 52428800; //50mb
|
|
public static int MAX_MESSAGE_SIZE = 4000;
|
|
public static int MAX_REACTION_SIZE = 100;
|
|
|
|
public static Dictionary<string, string> mimeTypes = new()
|
|
{
|
|
["unknown"] = "application/octet-stream",
|
|
["html"] = "text/html",
|
|
["htm"] = "text/html",
|
|
["css"] = "text/css",
|
|
["js"] = "application/javascript",
|
|
["mjs"] = "application/javascript",
|
|
["json"] = "application/json",
|
|
["xml"] = "application/xml",
|
|
["txt"] = "text/plain",
|
|
["csv"] = "text/csv",
|
|
["md"] = "text/markdown",
|
|
|
|
["png"] = "image/png",
|
|
["jpg"] = "image/jpeg",
|
|
["jpeg"] = "image/jpeg",
|
|
["gif"] = "image/gif",
|
|
["webp"] = "image/webp",
|
|
["svg"] = "image/svg+xml",
|
|
["ico"] = "image/x-icon",
|
|
["bmp"] = "image/bmp",
|
|
["tiff"] = "image/tiff",
|
|
["avif"] = "image/avif",
|
|
|
|
["mp3"] = "audio/mpeg",
|
|
["wav"] = "audio/wav",
|
|
["ogg"] = "audio/ogg",
|
|
["oga"] = "audio/ogg",
|
|
["weba"] = "audio/webm",
|
|
["aac"] = "audio/aac",
|
|
["flac"] = "audio/flac",
|
|
|
|
["mp4"] = "video/mp4",
|
|
["webm"] = "video/webm",
|
|
["mkv"] = "video/x-matroska",
|
|
["mov"] = "video/quicktime",
|
|
["avi"] = "video/x-msvideo",
|
|
["wmv"] = "video/x-ms-wmv",
|
|
|
|
["pdf"] = "application/pdf",
|
|
["zip"] = "application/zip",
|
|
["gz"] = "application/gzip",
|
|
["tar"] = "application/x-tar",
|
|
["rar"] = "application/vnd.rar",
|
|
["7z"] = "application/x-7z-compressed",
|
|
|
|
["woff"] = "font/woff",
|
|
["woff2"] = "font/woff2",
|
|
["ttf"] = "font/ttf",
|
|
["otf"] = "font/otf",
|
|
["eot"] = "application/vnd.ms-fontobject",
|
|
|
|
["wasm"] = "application/wasm",
|
|
|
|
["doc"] = "application/msword",
|
|
["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
["xls"] = "application/vnd.ms-excel",
|
|
["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
["ppt"] = "application/vnd.ms-powerpoint",
|
|
["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
};
|
|
|
|
public static ConcurrentDictionary<string, List<System.Net.WebSockets.WebSocket>> ActiveWebSockets = new();
|
|
public static ConcurrentDictionary<string, List<string>> ActiveCalls = new();
|
|
|
|
public static async Task WsHandler(HttpContext context, System.Net.WebSockets.WebSocket webSocket, IQueryCollection query)
|
|
{
|
|
var buffer = new byte[8192];
|
|
|
|
//odbierz PIERWSZĄ pełną ramkę (moze byc pofragmentowana)
|
|
int totalRead = 0;
|
|
System.Net.WebSockets.WebSocketReceiveResult result;
|
|
while (true)
|
|
{
|
|
if (totalRead >= buffer.Length)
|
|
{
|
|
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.MessageTooBig, "Auth too large", CancellationToken.None);
|
|
return;
|
|
}
|
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, totalRead, buffer.Length - totalRead), CancellationToken.None);
|
|
totalRead += result.Count;
|
|
if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close)
|
|
{
|
|
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
|
return;
|
|
}
|
|
if (result.EndOfMessage) break;
|
|
}
|
|
|
|
string authMessage = Encoding.UTF8.GetString(buffer, 0, totalRead);
|
|
Universal2String authParams;
|
|
try {
|
|
authParams = JsonSerializer.Deserialize<Universal2String>(authMessage, AppJsonSerializerContext.Default.Universal2String);
|
|
} catch {
|
|
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.InvalidMessageType, "Invalid auth", CancellationToken.None);
|
|
return;
|
|
}
|
|
|
|
string id = Account.Utils.GetValidIdOrZero(authParams.string1);
|
|
|
|
//rate limit prób autoryzacji WS (konto+IP) - wspólny zasobnik z /auth
|
|
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
|
if (RateLimited(AuthRateLimits, $"{id}:{clientIp}", AUTH_RATE_LIMIT))
|
|
{
|
|
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.PolicyViolation, "Rate limited", CancellationToken.None);
|
|
return;
|
|
}
|
|
|
|
string password = await Account.Utils.GetPassword(id);
|
|
string secret = await Account.Utils.NonceDecryptBody(id, password, authParams.string2);
|
|
|
|
string auth = await Account.Utils.Auth(id, password, secret);
|
|
|
|
if (auth != Account.Utils.LOGIN_SUCCESS)
|
|
{
|
|
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.PolicyViolation, "Unauthorized", CancellationToken.None);
|
|
return;
|
|
}
|
|
|
|
ActiveWebSockets.AddOrUpdate(id, new List<System.Net.WebSockets.WebSocket> { webSocket }, (k, list) => {
|
|
lock(list) {
|
|
list.Add(webSocket);
|
|
}
|
|
return list;
|
|
});
|
|
|
|
try
|
|
{
|
|
while (webSocket.State == System.Net.WebSockets.WebSocketState.Open)
|
|
{
|
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
|
if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
finally
|
|
{
|
|
if (ActiveWebSockets.TryGetValue(id, out var list))
|
|
{
|
|
lock(list) {
|
|
list.Remove(webSocket);
|
|
}
|
|
}
|
|
WsSendLocks.TryRemove(webSocket, out _);
|
|
if (webSocket.State == System.Net.WebSockets.WebSocketState.Open)
|
|
{
|
|
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
|
}
|
|
|
|
// Check if user was in any call and remove them
|
|
foreach (var kvp in ActiveCalls)
|
|
{
|
|
bool removed = false;
|
|
bool removeEntry = false;
|
|
lock (kvp.Value)
|
|
{
|
|
if (kvp.Value.Contains(id))
|
|
{
|
|
kvp.Value.Remove(id);
|
|
removed = true;
|
|
}
|
|
if (kvp.Value.Count == 0) removeEntry = true;
|
|
}
|
|
|
|
if (removed)
|
|
{
|
|
string callState;
|
|
lock (kvp.Value) { callState = JsonSerializer.Serialize(kvp.Value, AppJsonSerializerContext.Default.ListString); }
|
|
|
|
// Broadcast to all room members
|
|
string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{kvp.Key}/members");
|
|
if (members.Length == 0) members = Fs.ReadDirectory($"{ROOMS_DIR}/groups/{DOMAIN}/{kvp.Key}/members");
|
|
|
|
foreach (string memberWD in members)
|
|
{
|
|
if (Account.Utils.IsUserLocal(memberWD, out string _))
|
|
{
|
|
string memberId = Account.Utils.GetValidIdOrZero(memberWD);
|
|
_ = SendWsMessage(memberId, $"call_update:{kvp.Key}:{callState}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (removeEntry)
|
|
{
|
|
//S1: przycinaj puste wpisy - bez tego dictionary rósł o każdy
|
|
//pokój, w którym kiedykolwiek była rozmowa, a każdy disconnect
|
|
//skanował wszystkie historyczne wpisy
|
|
ActiveCalls.TryRemove(kvp.Key, out _);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Jeden outstanding SendAsync na WebSocket - semafory per-socket serializują wysyłkę
|
|
//(równoległe SendAsync na tym samym sockecie rzucają InvalidOperationException).
|
|
public static readonly ConcurrentDictionary<System.Net.WebSockets.WebSocket, SemaphoreSlim> WsSendLocks = new();
|
|
|
|
//Timeout pojedynczej wysyłki WS. Bez niego klient, który uwierzytelnił się
|
|
//a potem przestał odbierać TCP (zero-window), zawiesza SendAsync w
|
|
//nieskończoność: semafor per-socket zajęty na zawsze => wieczne waitery
|
|
//(pamięć), a await siedzi wewnątrz userLock/roomLock i blokuje całe ścieżki
|
|
//żądań tego użytkownika/pokoju (audyt W2)
|
|
public static readonly TimeSpan WsSendTimeout = TimeSpan.FromMinutes(1);
|
|
|
|
private static SemaphoreSlim GetWsSendLock(System.Net.WebSockets.WebSocket ws)
|
|
{
|
|
return WsSendLocks.GetOrAdd(ws, _ => new SemaphoreSlim(1, 1));
|
|
}
|
|
|
|
public static async Task SendWsMessage(string id, string message)
|
|
{
|
|
if (!ActiveWebSockets.TryGetValue(id, out var list))
|
|
{
|
|
return;
|
|
}
|
|
|
|
byte[] bytes;
|
|
System.Net.WebSockets.WebSocket[] snapshot;
|
|
lock (list)
|
|
{
|
|
if (list.Count == 0) return;
|
|
bytes = Encoding.UTF8.GetBytes(message);
|
|
snapshot = list.ToArray();
|
|
}
|
|
|
|
foreach (var ws in snapshot)
|
|
{
|
|
bool remove = false;
|
|
var sendLock = GetWsSendLock(ws);
|
|
await sendLock.WaitAsync();
|
|
try
|
|
{
|
|
if (ws.State == System.Net.WebSockets.WebSocketState.Open)
|
|
{
|
|
try
|
|
{
|
|
using var sendCts = new System.Threading.CancellationTokenSource(WsSendTimeout);
|
|
await ws.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, sendCts.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
//Timeout wysyłki = socket przestał odbierać - usuwamy go
|
|
//z aktywnych, żeby nie blokował semafora na zawsze
|
|
remove = true;
|
|
}
|
|
catch
|
|
{
|
|
remove = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
remove = true;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
sendLock.Release();
|
|
}
|
|
|
|
if (remove)
|
|
{
|
|
lock (list)
|
|
{
|
|
list.Remove(ws);
|
|
}
|
|
WsSendLocks.TryRemove(ws, out _);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static async Task<string> LoadBody(StreamReader reader)
|
|
{
|
|
StringBuilder bodyBuilder = new();
|
|
char[] buffer = new char[8192];
|
|
|
|
int read;
|
|
int totalRead = 0;
|
|
|
|
while ((read = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
|
{
|
|
totalRead += read;
|
|
|
|
if (totalRead > MAX_BODY_SIZE)
|
|
throw new Exception($"error:body.too.large:{MAX_BODY_SIZE / 1024 / 1024}");
|
|
|
|
bodyBuilder.Append(buffer, 0, read);
|
|
}
|
|
|
|
return bodyBuilder.ToString();
|
|
}
|
|
|
|
public static async Task<byte[]> LoadBodyBytes(Stream bodyStream, int maxBodySize)
|
|
{
|
|
using MemoryStream ms = new MemoryStream();
|
|
byte[] buffer = new byte[8192];
|
|
int read;
|
|
int totalRead = 0;
|
|
|
|
while ((read = await bodyStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
|
{
|
|
totalRead += read;
|
|
if (totalRead > maxBodySize)
|
|
throw new Exception($"error:body.too.large:{maxBodySize / 1024 / 1024}");
|
|
|
|
ms.Write(buffer, 0, read);
|
|
}
|
|
|
|
return ms.ToArray();
|
|
}
|
|
|
|
public static long GetDirectorySize(string directoryPath)
|
|
{
|
|
if (!Directory.Exists(directoryPath)) return 0;
|
|
|
|
long size = 0;
|
|
DirectoryInfo d = new DirectoryInfo(directoryPath);
|
|
foreach (FileInfo fi in d.GetFiles("*", SearchOption.AllDirectories))
|
|
{
|
|
size += fi.Length;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
public static async Task LoadEnv()
|
|
{
|
|
if (File.Exists(".env"))
|
|
{
|
|
|
|
foreach (var line in File.ReadAllLines(".env"))
|
|
{
|
|
var trimmed = line.Trim();
|
|
if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("#")) continue;
|
|
|
|
var parts = trimmed.Split('=', 2, StringSplitOptions.None);
|
|
if (parts.Length == 2)
|
|
{
|
|
var key = parts[0].Trim();
|
|
var val = parts[1];
|
|
int commentIndex = val.IndexOf('#');
|
|
if (commentIndex >= 0)
|
|
{
|
|
val = val.Substring(0, commentIndex);
|
|
}
|
|
val = val.Trim();
|
|
|
|
Environment.SetEnvironmentVariable(key, val);
|
|
}
|
|
}
|
|
}
|
|
|
|
string? port = Environment.GetEnvironmentVariable("PORT");
|
|
if (port != null)
|
|
{
|
|
PORT = int.Parse(port);
|
|
}
|
|
|
|
string? actionRateLimitStr = Environment.GetEnvironmentVariable("ACTION_RATE_LIMIT");
|
|
if (actionRateLimitStr != null && int.TryParse(actionRateLimitStr, out int parsedLimit))
|
|
{
|
|
ACTION_RATE_LIMIT = parsedLimit;
|
|
}
|
|
|
|
string? voiceAuthRateLimitStr = Environment.GetEnvironmentVariable("VOICE_AUTH_RATE_LIMIT");
|
|
if (voiceAuthRateLimitStr != null && int.TryParse(voiceAuthRateLimitStr, out int parsedVoiceAuthLimit))
|
|
{
|
|
VOICE_AUTH_RATE_LIMIT = parsedVoiceAuthLimit;
|
|
}
|
|
|
|
AUTH_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("AUTH_RATE_LIMIT"), out int parsedAuthLimit) ? parsedAuthLimit : AUTH_RATE_LIMIT;
|
|
NONCE_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("NONCE_RATE_LIMIT"), out int parsedNonceLimit) ? parsedNonceLimit : NONCE_RATE_LIMIT;
|
|
KDFSALT_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("KDFSALT_RATE_LIMIT"), out int parsedKdfSaltLimit) ? parsedKdfSaltLimit : KDFSALT_RATE_LIMIT;
|
|
ACCOUNT_INIT_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("ACCOUNT_INIT_RATE_LIMIT"), out int parsedAccountInitLimit) ? parsedAccountInitLimit : ACCOUNT_INIT_RATE_LIMIT;
|
|
|
|
VOICE_IPS.Clear();
|
|
|
|
port = Environment.GetEnvironmentVariable("VOICE_IPS");
|
|
if (port != null)
|
|
{
|
|
foreach (var address in port.Split(","))
|
|
{
|
|
VOICE_IPS.Add(address);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
port = Environment.GetEnvironmentVariable("PORT_VOICE");
|
|
if (port != null)
|
|
{
|
|
VOICE_IPS.Add($"127.0.0.1:{port}");
|
|
}
|
|
}
|
|
|
|
DOMAIN = Environment.GetEnvironmentVariable("DOMAIN") ?? DOMAIN;
|
|
SERVER_KEY = Environment.GetEnvironmentVariable("SERVER_KEY") ?? SERVER_KEY;
|
|
VOICE_KEY = Environment.GetEnvironmentVariable("VOICE_KEY") ?? VOICE_KEY;
|
|
DATA_DIR = Environment.GetEnvironmentVariable("DATA_DIR") ?? DATA_DIR;
|
|
OWNER_CONTACT = Environment.GetEnvironmentVariable("OWNER_CONTACT") ?? OWNER_CONTACT;
|
|
|
|
CACHE_ENTRY_SIZE = int.TryParse(Environment.GetEnvironmentVariable("CACHE_ENTRY_SIZE"), out var res) ? res : CACHE_ENTRY_SIZE;
|
|
FILE_CACHE_SIZE = int.TryParse(Environment.GetEnvironmentVariable("FILE_CACHE_SIZE"), out res) ? res : FILE_CACHE_SIZE;
|
|
EXIST_CACHE_SIZE = int.TryParse(Environment.GetEnvironmentVariable("EXIST_CACHE_SIZE"), out res) ? res : EXIST_CACHE_SIZE;
|
|
DIR_CACHE_SIZE = int.TryParse(Environment.GetEnvironmentVariable("DIR_CACHE_SIZE"), out res) ? res : DIR_CACHE_SIZE;
|
|
LOCK_SIZE = int.TryParse(Environment.GetEnvironmentVariable("LOCK_SIZE"), out res) ? res : LOCK_SIZE;
|
|
|
|
PRIVATE_STORAGE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("PRIVATE_STORAGE_LIMIT"), out res) ? res : PRIVATE_STORAGE_LIMIT;
|
|
PUBLIC_STORAGE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("PUBLIC_STORAGE_LIMIT"), out res) ? res : PUBLIC_STORAGE_LIMIT;
|
|
MAX_BODY_SIZE = int.TryParse(Environment.GetEnvironmentVariable("MAX_BODY_SIZE"), out res) ? res : MAX_BODY_SIZE;
|
|
ATTACHMENT_SIZE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("ATTACHMENT_SIZE_LIMIT"), out res) ? res : ATTACHMENT_SIZE_LIMIT;
|
|
MAX_MESSAGE_SIZE = int.TryParse(Environment.GetEnvironmentVariable("MAX_MESSAGE_SIZE"), out res) ? res : MAX_MESSAGE_SIZE;
|
|
MAX_REACTION_SIZE = int.TryParse(Environment.GetEnvironmentVariable("MAX_REACTION_SIZE"), out res) ? res : MAX_REACTION_SIZE;
|
|
|
|
ROOMS_DIR = DATA_DIR + "/rooms";
|
|
ACCOUNTS_DIR = DATA_DIR + "/accounts";
|
|
ACCOUNTS_NAME_DIR = ACCOUNTS_DIR + "/name";
|
|
ACCOUNTS_DATA_DIR = ACCOUNTS_DIR + "/data";
|
|
ACCOUNTS_FREEID_DIR = ACCOUNTS_DIR + "/freeid";
|
|
|
|
if (!Fs.Exists(DATA_DIR))
|
|
{
|
|
Fs.CreateDirectory(DATA_DIR);
|
|
}
|
|
|
|
if (!Fs.Exists(ROOMS_DIR))
|
|
{
|
|
Fs.CreateDirectory(ROOMS_DIR);
|
|
}
|
|
if (!Fs.Exists(ROOMS_DIR + "/dms"))
|
|
Fs.CreateDirectory(ROOMS_DIR + "/dms");
|
|
if (!Fs.Exists(ROOMS_DIR + "/dms_staging"))
|
|
Fs.CreateDirectory(ROOMS_DIR + "/dms_staging");
|
|
if (!Fs.Exists(ROOMS_DIR + "/groups"))
|
|
{
|
|
Fs.CreateDirectory(ROOMS_DIR + "/groups");
|
|
}
|
|
if (!Fs.Exists(ROOMS_DIR + "/spaces"))
|
|
{
|
|
Fs.CreateDirectory(ROOMS_DIR + "/spaces");
|
|
}
|
|
if (!Fs.Exists(ROOMS_DIR + "/freegroups"))
|
|
{
|
|
Fs.CreateDirectory(ROOMS_DIR + "/freegroups");
|
|
}
|
|
if (!Fs.Exists(ROOMS_DIR + "/freespaces"))
|
|
{
|
|
Fs.CreateDirectory(ROOMS_DIR + "/freespaces");
|
|
}
|
|
|
|
if (!Fs.Exists(ROOMS_DIR + "/lastspace"))
|
|
{
|
|
await Fs.WriteFile(ROOMS_DIR + "/lastspace", Encoding.UTF8.GetBytes("0"));
|
|
}
|
|
|
|
if (!Fs.Exists(ACCOUNTS_DIR))
|
|
{
|
|
Fs.CreateDirectory(ACCOUNTS_DIR);
|
|
}
|
|
if (!Fs.Exists(ACCOUNTS_NAME_DIR))
|
|
{
|
|
Fs.CreateDirectory(ACCOUNTS_NAME_DIR);
|
|
}
|
|
if (!Fs.Exists(ACCOUNTS_DATA_DIR))
|
|
{
|
|
Fs.CreateDirectory(ACCOUNTS_DATA_DIR);
|
|
}
|
|
if (!Fs.Exists(ACCOUNTS_FREEID_DIR))
|
|
{
|
|
Fs.CreateDirectory(ACCOUNTS_FREEID_DIR);
|
|
}
|
|
|
|
if (!Fs.Exists(ACCOUNTS_DIR + "/last"))
|
|
{
|
|
await Fs.WriteFile(ACCOUNTS_DIR + "/last", Encoding.UTF8.GetBytes("0"));
|
|
}
|
|
if (!Fs.Exists($"{ACCOUNTS_DIR}/registration"))
|
|
{
|
|
await Fs.WriteFile($"{ACCOUNTS_DIR}/registration", Encoding.UTF8.GetBytes("first;"));
|
|
}
|
|
}
|
|
} |