Initial
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
namespace LightlessSyncAuthService.Authentication;
|
||||
|
||||
public record SecretKeyAuthReply(bool Success, string? Uid, string? PrimaryUid, string? Alias, bool TempBan, bool Permaban, bool MarkedForBan);
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace LightlessSyncAuthService.Authentication;
|
||||
|
||||
internal record SecretKeyFailedAuthorization
|
||||
{
|
||||
private int failedAttempts = 1;
|
||||
public int FailedAttempts => failedAttempts;
|
||||
public Task ResetTask { get; set; }
|
||||
public void IncreaseFailedAttempts()
|
||||
{
|
||||
Interlocked.Increment(ref failedAttempts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using LightlessSyncAuthService.Authentication;
|
||||
using LightlessSyncAuthService.Services;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StackExchange.Redis;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSyncAuthService.Controllers;
|
||||
|
||||
public abstract class AuthControllerBase : Controller
|
||||
{
|
||||
protected readonly ILogger Logger;
|
||||
protected readonly IHttpContextAccessor HttpAccessor;
|
||||
protected readonly IConfigurationService<AuthServiceConfiguration> Configuration;
|
||||
protected readonly IDbContextFactory<LightlessDbContext> LightlessDbContextFactory;
|
||||
protected readonly SecretKeyAuthenticatorService SecretKeyAuthenticatorService;
|
||||
private readonly IDatabase _redis;
|
||||
private readonly GeoIPService _geoIPProvider;
|
||||
|
||||
protected AuthControllerBase(ILogger logger,
|
||||
IHttpContextAccessor accessor, IDbContextFactory<LightlessDbContext> lightlessDbContextFactory,
|
||||
SecretKeyAuthenticatorService secretKeyAuthenticatorService,
|
||||
IConfigurationService<AuthServiceConfiguration> configuration,
|
||||
IDatabase redisDb, GeoIPService geoIPProvider)
|
||||
{
|
||||
Logger = logger;
|
||||
HttpAccessor = accessor;
|
||||
_redis = redisDb;
|
||||
_geoIPProvider = geoIPProvider;
|
||||
LightlessDbContextFactory = lightlessDbContextFactory;
|
||||
SecretKeyAuthenticatorService = secretKeyAuthenticatorService;
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
protected async Task<IActionResult> GenericAuthResponse(LightlessDbContext dbContext, string charaIdent, SecretKeyAuthReply authResult)
|
||||
{
|
||||
if (await IsIdentBanned(dbContext, charaIdent))
|
||||
{
|
||||
Logger.LogWarning("Authenticate:IDENTBAN:{id}:{ident}", authResult.Uid, charaIdent);
|
||||
return Unauthorized("Your character is banned from using the service.");
|
||||
}
|
||||
|
||||
if (!authResult.Success && !authResult.TempBan)
|
||||
{
|
||||
Logger.LogWarning("Authenticate:INVALID:{id}:{ident}", authResult?.Uid ?? "NOUID", charaIdent);
|
||||
return Unauthorized("The provided secret key is invalid. Verify your Lightless accounts existence and/or recover the secret key.");
|
||||
}
|
||||
if (!authResult.Success && authResult.TempBan)
|
||||
{
|
||||
Logger.LogWarning("Authenticate:TEMPBAN:{id}:{ident}", authResult.Uid ?? "NOUID", charaIdent);
|
||||
return Unauthorized("Due to an excessive amount of failed authentication attempts you are temporarily locked out. Check your Secret Key configuration and try connecting again in 5 minutes.");
|
||||
}
|
||||
|
||||
if (authResult.Permaban || authResult.MarkedForBan)
|
||||
{
|
||||
if (authResult.MarkedForBan)
|
||||
{
|
||||
Logger.LogWarning("Authenticate:MARKBAN:{id}:{primaryid}:{ident}", authResult.Uid, authResult.PrimaryUid, charaIdent);
|
||||
await EnsureBan(authResult.Uid!, authResult.PrimaryUid, charaIdent);
|
||||
}
|
||||
|
||||
Logger.LogWarning("Authenticate:UIDBAN:{id}:{ident}", authResult.Uid, charaIdent);
|
||||
return Unauthorized("Your Lightless account is banned from using the service.");
|
||||
}
|
||||
|
||||
var existingIdent = await _redis.StringGetAsync("UID:" + authResult.Uid);
|
||||
if (!string.IsNullOrEmpty(existingIdent))
|
||||
{
|
||||
Logger.LogWarning("Authenticate:DUPLICATE:{id}:{ident}", authResult.Uid, charaIdent);
|
||||
return Unauthorized("Already logged in to this Lightless account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game.");
|
||||
}
|
||||
|
||||
Logger.LogInformation("Authenticate:SUCCESS:{id}:{ident}", authResult.Uid, charaIdent);
|
||||
return await CreateJwtFromId(authResult.Uid!, charaIdent, authResult.Alias ?? string.Empty);
|
||||
}
|
||||
|
||||
protected JwtSecurityToken CreateJwt(IEnumerable<Claim> authClaims)
|
||||
{
|
||||
var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration.GetValue<string>(nameof(LightlessConfigurationBase.Jwt))));
|
||||
|
||||
var token = new SecurityTokenDescriptor()
|
||||
{
|
||||
Subject = new ClaimsIdentity(authClaims),
|
||||
SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature),
|
||||
Expires = new(long.Parse(authClaims.First(f => string.Equals(f.Type, LightlessClaimTypes.Expires, StringComparison.Ordinal)).Value!, CultureInfo.InvariantCulture), DateTimeKind.Utc),
|
||||
};
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
return handler.CreateJwtSecurityToken(token);
|
||||
}
|
||||
|
||||
protected async Task<IActionResult> CreateJwtFromId(string uid, string charaIdent, string alias)
|
||||
{
|
||||
var token = CreateJwt(new List<Claim>()
|
||||
{
|
||||
new Claim(LightlessClaimTypes.Uid, uid),
|
||||
new Claim(LightlessClaimTypes.CharaIdent, charaIdent),
|
||||
new Claim(LightlessClaimTypes.Alias, alias),
|
||||
new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddHours(6).Ticks.ToString(CultureInfo.InvariantCulture)),
|
||||
new Claim(LightlessClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(HttpAccessor))
|
||||
});
|
||||
|
||||
return Content(token.RawData);
|
||||
}
|
||||
|
||||
protected async Task EnsureBan(string uid, string? primaryUid, string charaIdent)
|
||||
{
|
||||
using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync();
|
||||
if (!dbContext.BannedUsers.Any(c => c.CharacterIdentification == charaIdent))
|
||||
{
|
||||
dbContext.BannedUsers.Add(new Banned()
|
||||
{
|
||||
CharacterIdentification = charaIdent,
|
||||
Reason = "Autobanned CharacterIdent (" + uid + ")",
|
||||
});
|
||||
}
|
||||
|
||||
var uidToLookFor = primaryUid ?? uid;
|
||||
|
||||
var primaryUserAuth = await dbContext.Auth.FirstAsync(f => f.UserUID == uidToLookFor);
|
||||
primaryUserAuth.MarkForBan = false;
|
||||
primaryUserAuth.IsBanned = true;
|
||||
|
||||
var lodestone = await dbContext.LodeStoneAuth.Include(a => a.User).FirstOrDefaultAsync(c => c.User.UID == uidToLookFor);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.HashedLodestoneId))
|
||||
{
|
||||
dbContext.BannedRegistrations.Add(new BannedRegistrations()
|
||||
{
|
||||
DiscordIdOrLodestoneAuth = lodestone.HashedLodestoneId,
|
||||
});
|
||||
}
|
||||
if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.DiscordId.ToString()))
|
||||
{
|
||||
dbContext.BannedRegistrations.Add(new BannedRegistrations()
|
||||
{
|
||||
DiscordIdOrLodestoneAuth = lodestone.DiscordId.ToString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
protected async Task<bool> IsIdentBanned(LightlessDbContext dbContext, string charaIdent)
|
||||
{
|
||||
return await dbContext.BannedUsers.AsNoTracking().AnyAsync(u => u.CharacterIdentification == charaIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using LightlessSync.API.Routes;
|
||||
using LightlessSyncAuthService.Services;
|
||||
using LightlessSyncShared;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace LightlessSyncAuthService.Controllers;
|
||||
|
||||
[Route(LightlessAuth.Auth)]
|
||||
public class JwtController : AuthControllerBase
|
||||
{
|
||||
public JwtController(ILogger<JwtController> logger,
|
||||
IHttpContextAccessor accessor, IDbContextFactory<LightlessDbContext> lightlessDbContextFactory,
|
||||
SecretKeyAuthenticatorService secretKeyAuthenticatorService,
|
||||
IConfigurationService<AuthServiceConfiguration> configuration,
|
||||
IDatabase redisDb, GeoIPService geoIPProvider)
|
||||
: base(logger, accessor, lightlessDbContextFactory, secretKeyAuthenticatorService,
|
||||
configuration, redisDb, geoIPProvider)
|
||||
{
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost(LightlessAuth.Auth_CreateIdent)]
|
||||
public async Task<IActionResult> CreateToken(string auth, string charaIdent)
|
||||
{
|
||||
using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync();
|
||||
return await AuthenticateInternal(dbContext, auth, charaIdent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
[HttpGet(LightlessAuth.Auth_RenewToken)]
|
||||
public async Task<IActionResult> RenewToken()
|
||||
{
|
||||
using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync();
|
||||
try
|
||||
{
|
||||
var uid = HttpContext.User.Claims.Single(p => string.Equals(p.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))!.Value;
|
||||
var ident = HttpContext.User.Claims.Single(p => string.Equals(p.Type, LightlessClaimTypes.CharaIdent, StringComparison.Ordinal))!.Value;
|
||||
var alias = HttpContext.User.Claims.SingleOrDefault(p => string.Equals(p.Type, LightlessClaimTypes.Alias))?.Value ?? string.Empty;
|
||||
|
||||
if (await dbContext.Auth.Where(u => u.UserUID == uid || u.PrimaryUserUID == uid).AnyAsync(a => a.MarkForBan))
|
||||
{
|
||||
var userAuth = await dbContext.Auth.SingleAsync(u => u.UserUID == uid);
|
||||
await EnsureBan(uid, userAuth.PrimaryUserUID, ident);
|
||||
|
||||
return Unauthorized("Your Lightless account is banned.");
|
||||
}
|
||||
|
||||
if (await IsIdentBanned(dbContext, ident))
|
||||
{
|
||||
return Unauthorized("Your XIV service account is banned from using the service.");
|
||||
}
|
||||
|
||||
Logger.LogInformation("RenewToken:SUCCESS:{id}:{ident}", uid, ident);
|
||||
return await CreateJwtFromId(uid, ident, alias);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "RenewToken:FAILURE");
|
||||
return Unauthorized("Unknown error while renewing authentication token");
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task<IActionResult> AuthenticateInternal(LightlessDbContext dbContext, string auth, string charaIdent)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(auth)) return BadRequest("No Authkey");
|
||||
if (string.IsNullOrEmpty(charaIdent)) return BadRequest("No CharaIdent");
|
||||
|
||||
var ip = HttpAccessor.GetIpAddress();
|
||||
|
||||
var authResult = await SecretKeyAuthenticatorService.AuthorizeAsync(ip, auth);
|
||||
|
||||
return await GenericAuthResponse(dbContext, charaIdent, authResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Authenticate:UNKNOWN");
|
||||
return Unauthorized("Unknown internal server error during authentication");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
using LightlessSync.API.Routes;
|
||||
using LightlessSyncAuthService.Services;
|
||||
using LightlessSyncShared;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
|
||||
namespace LightlessSyncAuthService.Controllers;
|
||||
|
||||
[Route(LightlessAuth.OAuth)]
|
||||
public class OAuthController : AuthControllerBase
|
||||
{
|
||||
private const string _discordOAuthCall = "discordCall";
|
||||
private const string _discordOAuthCallback = "discordCallback";
|
||||
private static readonly ConcurrentDictionary<string, string> _cookieOAuthResponse = [];
|
||||
|
||||
public OAuthController(ILogger<OAuthController> logger,
|
||||
IHttpContextAccessor accessor, IDbContextFactory<LightlessDbContext> lightlessDbContext,
|
||||
SecretKeyAuthenticatorService secretKeyAuthenticatorService,
|
||||
IConfigurationService<AuthServiceConfiguration> configuration,
|
||||
IDatabase redisDb, GeoIPService geoIPProvider)
|
||||
: base(logger, accessor, lightlessDbContext, secretKeyAuthenticatorService,
|
||||
configuration, redisDb, geoIPProvider)
|
||||
{
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet(_discordOAuthCall)]
|
||||
public IActionResult DiscordOAuthSetCookieAndRedirect([FromQuery] string sessionId)
|
||||
{
|
||||
var discordOAuthUri = Configuration.GetValueOrDefault<Uri?>(nameof(AuthServiceConfiguration.PublicOAuthBaseUri), null);
|
||||
var discordClientSecret = Configuration.GetValueOrDefault<string?>(nameof(AuthServiceConfiguration.DiscordOAuthClientSecret), null);
|
||||
var discordClientId = Configuration.GetValueOrDefault<string?>(nameof(AuthServiceConfiguration.DiscordOAuthClientId), null);
|
||||
if (discordClientSecret == null || discordClientId == null || discordOAuthUri == null)
|
||||
return BadRequest("Server does not support OAuth2");
|
||||
|
||||
Logger.LogDebug("Starting OAuth Process for {session}", sessionId);
|
||||
|
||||
var cookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
Expires = DateTime.UtcNow.AddMinutes(30)
|
||||
};
|
||||
Response.Cookies.Append("DiscordOAuthSessionCookie", sessionId, cookieOptions);
|
||||
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "client_id", discordClientId },
|
||||
{ "response_type", "code" },
|
||||
{ "redirect_uri", new Uri(discordOAuthUri, _discordOAuthCallback).ToString() },
|
||||
{ "scope", "identify"},
|
||||
};
|
||||
using var content = new FormUrlEncodedContent(parameters);
|
||||
UriBuilder builder = new UriBuilder("https://discord.com/oauth2/authorize");
|
||||
var query = HttpUtility.ParseQueryString(builder.Query);
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
query[param.Key] = param.Value;
|
||||
}
|
||||
builder.Query = query.ToString();
|
||||
|
||||
return Redirect(builder.ToString());
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet(_discordOAuthCallback)]
|
||||
public async Task<IActionResult> DiscordOAuthCallback([FromQuery] string code)
|
||||
{
|
||||
var reqId = Request.Cookies["DiscordOAuthSessionCookie"];
|
||||
|
||||
var discordOAuthUri = Configuration.GetValueOrDefault<Uri?>(nameof(AuthServiceConfiguration.PublicOAuthBaseUri), null);
|
||||
var discordClientSecret = Configuration.GetValueOrDefault<string?>(nameof(AuthServiceConfiguration.DiscordOAuthClientSecret), null);
|
||||
var discordClientId = Configuration.GetValueOrDefault<string?>(nameof(AuthServiceConfiguration.DiscordOAuthClientId), null);
|
||||
if (discordClientSecret == null || discordClientId == null || discordOAuthUri == null)
|
||||
return BadRequest("Server does not support OAuth2");
|
||||
if (string.IsNullOrEmpty(reqId)) return BadRequest("No session cookie found");
|
||||
if (string.IsNullOrEmpty(code)) return BadRequest("No Discord OAuth2 code found");
|
||||
|
||||
Logger.LogDebug("Discord OAuth Callback for {session}", reqId);
|
||||
|
||||
var query = HttpUtility.ParseQueryString(discordOAuthUri.Query);
|
||||
using var client = new HttpClient();
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "client_id", discordClientId },
|
||||
{ "client_secret", discordClientSecret },
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "code", code },
|
||||
{ "redirect_uri", new Uri(discordOAuthUri, _discordOAuthCallback).ToString() }
|
||||
};
|
||||
|
||||
using var content = new FormUrlEncodedContent(parameters);
|
||||
using var response = await client.PostAsync("https://discord.com/api/oauth2/token", content);
|
||||
using var responseBody = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Logger.LogDebug("Failed to get Discord token for {session}", reqId);
|
||||
return BadRequest("Failed to get Discord token");
|
||||
}
|
||||
|
||||
using var tokenJson = await JsonDocument.ParseAsync(responseBody).ConfigureAwait(false);
|
||||
var token = tokenJson.RootElement.GetProperty("access_token").GetString();
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
using var meResponse = await httpClient.GetAsync("https://discord.com/api/users/@me");
|
||||
using var meBody = await meResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
|
||||
if (!meResponse.IsSuccessStatusCode)
|
||||
{
|
||||
Logger.LogDebug("Failed to get Discord user info for {session}", reqId);
|
||||
return BadRequest("Failed to get Discord user info");
|
||||
}
|
||||
|
||||
ulong discordUserId = 0;
|
||||
string discordUserName = string.Empty;
|
||||
try
|
||||
{
|
||||
using var jsonResponse = await JsonDocument.ParseAsync(meBody).ConfigureAwait(false);
|
||||
discordUserId = ulong.Parse(jsonResponse.RootElement.GetProperty("id").GetString()!);
|
||||
discordUserName = jsonResponse.RootElement.GetProperty("username").GetString()!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to parse Discord user info for {session}", reqId);
|
||||
return BadRequest("Failed to parse user id from @me response for token");
|
||||
}
|
||||
|
||||
if (discordUserId == 0)
|
||||
return BadRequest("Failed to get Discord ID from login token");
|
||||
|
||||
using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var lightlessUser = await dbContext.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == discordUserId);
|
||||
if (lightlessUser == default)
|
||||
{
|
||||
Logger.LogDebug("Failed to get Lightless user for {session}, DiscordId: {id}", reqId, discordUserId);
|
||||
|
||||
return BadRequest("Could not find a Lightless user associated to this Discord account.");
|
||||
}
|
||||
|
||||
JwtSecurityToken? jwt = null;
|
||||
try
|
||||
{
|
||||
jwt = CreateJwt([
|
||||
new Claim(LightlessClaimTypes.Uid, lightlessUser.User!.UID),
|
||||
new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddDays(14).Ticks.ToString(CultureInfo.InvariantCulture)),
|
||||
new Claim(LightlessClaimTypes.DiscordId, discordUserId.ToString()),
|
||||
new Claim(LightlessClaimTypes.DiscordUser, discordUserName),
|
||||
new Claim(LightlessClaimTypes.OAuthLoginToken, true.ToString())
|
||||
]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to create the OAuth2 token for session {session} and Discord user {user}", reqId, discordUserId);
|
||||
return BadRequest("Failed to create the OAuth2 token. Please contact the developer for more information.");
|
||||
}
|
||||
|
||||
_cookieOAuthResponse[reqId] = jwt.RawData;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
bool isRemoved = false;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
|
||||
if (!_cookieOAuthResponse.ContainsKey(reqId))
|
||||
{
|
||||
isRemoved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isRemoved)
|
||||
_cookieOAuthResponse.TryRemove(reqId, out _);
|
||||
});
|
||||
|
||||
Logger.LogDebug("Setting JWT response for {session}, process complete", reqId);
|
||||
return Ok("The OAuth2 token was generated. The plugin will grab it automatically. You can close this browser tab.");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "OAuthToken")]
|
||||
[HttpPost(LightlessAuth.OAuth_GetUIDsBasedOnSecretKeys)]
|
||||
public async Task<Dictionary<string, string>> GetUIDsBasedOnSecretKeys([FromBody] List<string> secretKeys)
|
||||
{
|
||||
if (!secretKeys.Any())
|
||||
return [];
|
||||
|
||||
using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync();
|
||||
|
||||
Dictionary<string, string> secretKeysToUIDDict = secretKeys.Distinct().ToDictionary(k => k, _ => string.Empty, StringComparer.Ordinal);
|
||||
foreach (var key in secretKeys)
|
||||
{
|
||||
var shaKey = StringUtils.Sha256String(key);
|
||||
var associatedAuth = await dbContext.Auth.AsNoTracking().SingleOrDefaultAsync(a => a.HashedKey == shaKey);
|
||||
if (associatedAuth != null)
|
||||
{
|
||||
secretKeysToUIDDict[key] = associatedAuth.UserUID;
|
||||
}
|
||||
}
|
||||
|
||||
return secretKeysToUIDDict;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "OAuthToken")]
|
||||
[HttpPost(LightlessAuth.OAuth_RenewOAuthToken)]
|
||||
public IActionResult RenewOAuthToken()
|
||||
{
|
||||
var claims = HttpContext.User.Claims.Where(c => c.Type != LightlessClaimTypes.Expires).ToList();
|
||||
claims.Add(new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddDays(14).Ticks.ToString(CultureInfo.InvariantCulture)));
|
||||
return Content(CreateJwt(claims).RawData);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet(LightlessAuth.OAuth_GetDiscordOAuthToken)]
|
||||
public async Task<IActionResult> GetDiscordOAuthToken([FromQuery] string sessionId)
|
||||
{
|
||||
Logger.LogDebug("Starting to wait for GetDiscordOAuthToken for {session}", sessionId);
|
||||
using CancellationTokenSource cts = new();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(55));
|
||||
while (!_cookieOAuthResponse.ContainsKey(sessionId) && !cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
|
||||
}
|
||||
if (cts.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogDebug("Timeout elapsed for {session}", sessionId);
|
||||
return BadRequest("Did not find Discord OAuth2 response");
|
||||
}
|
||||
_cookieOAuthResponse.TryRemove(sessionId, out var token);
|
||||
if (token == null)
|
||||
{
|
||||
Logger.LogDebug("No token found for {session}", sessionId);
|
||||
return BadRequest("OAuth session was never established");
|
||||
}
|
||||
Logger.LogDebug("Returning JWT for {session}, process complete", sessionId);
|
||||
return Content(token);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet(LightlessAuth.OAuth_GetDiscordOAuthEndpoint)]
|
||||
public Uri? GetDiscordOAuthEndpoint()
|
||||
{
|
||||
var discordOAuthUri = Configuration.GetValueOrDefault<Uri?>(nameof(AuthServiceConfiguration.PublicOAuthBaseUri), null);
|
||||
var discordClientSecret = Configuration.GetValueOrDefault<string?>(nameof(AuthServiceConfiguration.DiscordOAuthClientSecret), null);
|
||||
var discordClientId = Configuration.GetValueOrDefault<string?>(nameof(AuthServiceConfiguration.DiscordOAuthClientId), null);
|
||||
if (discordClientSecret == null || discordClientId == null || discordOAuthUri == null)
|
||||
return null;
|
||||
return new Uri(discordOAuthUri, _discordOAuthCall);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "OAuthToken")]
|
||||
[HttpGet(LightlessAuth.OAuth_GetUIDs)]
|
||||
public async Task<Dictionary<string, string>> GetAvailableUIDs()
|
||||
{
|
||||
string primaryUid = HttpContext.User.Claims.Single(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))!.Value;
|
||||
using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var lightlessUser = await dbContext.Auth.AsNoTracking().Include(u => u.User).FirstOrDefaultAsync(f => f.UserUID == primaryUid).ConfigureAwait(false);
|
||||
if (lightlessUser == null || lightlessUser.User == null) return [];
|
||||
var uid = lightlessUser.User.UID;
|
||||
var allUids = await dbContext.Auth.AsNoTracking().Include(u => u.User).Where(a => a.UserUID == uid || a.PrimaryUserUID == uid).ToListAsync().ConfigureAwait(false);
|
||||
var result = allUids.OrderBy(u => u.UserUID == uid ? 0 : 1).ThenBy(u => u.UserUID).Select(u => (u.UserUID, u.User.Alias)).ToDictionary();
|
||||
return result;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "OAuthToken")]
|
||||
[HttpPost(LightlessAuth.OAuth_CreateOAuth)]
|
||||
public async Task<IActionResult> CreateTokenWithOAuth(string uid, string charaIdent)
|
||||
{
|
||||
using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync();
|
||||
|
||||
return await AuthenticateOAuthInternal(dbContext, uid, charaIdent);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> AuthenticateOAuthInternal(LightlessDbContext dbContext, string requestedUid, string charaIdent)
|
||||
{
|
||||
try
|
||||
{
|
||||
string primaryUid = HttpContext.User.Claims.Single(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))!.Value;
|
||||
if (string.IsNullOrEmpty(requestedUid)) return BadRequest("No UID");
|
||||
if (string.IsNullOrEmpty(charaIdent)) return BadRequest("No CharaIdent");
|
||||
|
||||
var ip = HttpAccessor.GetIpAddress();
|
||||
|
||||
var authResult = await SecretKeyAuthenticatorService.AuthorizeOauthAsync(ip, primaryUid, requestedUid);
|
||||
|
||||
return await GenericAuthResponse(dbContext, charaIdent, authResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Authenticate:UNKNOWN");
|
||||
return Unauthorized("Unknown internal server error during authentication");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LightlessSyncShared\LightlessSyncShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
55
LightlessSyncServer/LightlessSyncAuthService/Program.cs
Normal file
55
LightlessSyncServer/LightlessSyncAuthService/Program.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace LightlessSyncAuthService;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var hostBuilder = CreateHostBuilder(args);
|
||||
using var host = hostBuilder.Build();
|
||||
try
|
||||
{
|
||||
host.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args)
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddConsole();
|
||||
});
|
||||
var logger = loggerFactory.CreateLogger<Startup>();
|
||||
return Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureAppConfiguration((ctx, config) =>
|
||||
{
|
||||
var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH");
|
||||
if (!string.IsNullOrEmpty(appSettingsPath))
|
||||
{
|
||||
config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
||||
}
|
||||
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseContentRoot(AppContext.BaseDirectory);
|
||||
webBuilder.ConfigureLogging((ctx, builder) =>
|
||||
{
|
||||
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
|
||||
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
|
||||
});
|
||||
webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:37726",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5056",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using LightlessSyncShared;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using MaxMind.GeoIP2;
|
||||
|
||||
namespace LightlessSyncAuthService.Services;
|
||||
|
||||
public class GeoIPService : IHostedService
|
||||
{
|
||||
private readonly ILogger<GeoIPService> _logger;
|
||||
private readonly IConfigurationService<AuthServiceConfiguration> _lightlessConfiguration;
|
||||
private bool _useGeoIP = false;
|
||||
private string _cityFile = string.Empty;
|
||||
private DatabaseReader? _dbReader;
|
||||
private DateTime _dbLastWriteTime = DateTime.MinValue;
|
||||
private CancellationTokenSource _fileWriteTimeCheckCts = new();
|
||||
private bool _processingReload = false;
|
||||
|
||||
public GeoIPService(ILogger<GeoIPService> logger,
|
||||
IConfigurationService<AuthServiceConfiguration> lightlessConfiguration)
|
||||
{
|
||||
_logger = logger;
|
||||
_lightlessConfiguration = lightlessConfiguration;
|
||||
}
|
||||
|
||||
public async Task<string> GetCountryFromIP(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
if (!_useGeoIP)
|
||||
{
|
||||
return "*";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ip = httpContextAccessor.GetIpAddress();
|
||||
|
||||
using CancellationTokenSource waitCts = new();
|
||||
waitCts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
while (_processingReload) await Task.Delay(100, waitCts.Token).ConfigureAwait(false);
|
||||
|
||||
if (_dbReader!.TryCity(ip, out var response))
|
||||
{
|
||||
string? continent = response?.Continent.Code;
|
||||
if (!string.IsNullOrEmpty(continent) &&
|
||||
string.Equals(continent, "NA", StringComparison.Ordinal)
|
||||
&& response?.Location.Longitude != null)
|
||||
{
|
||||
if (response.Location.Longitude < -102)
|
||||
{
|
||||
continent = "NA-W";
|
||||
}
|
||||
else
|
||||
{
|
||||
continent = "NA-E";
|
||||
}
|
||||
}
|
||||
|
||||
return continent ?? "*";
|
||||
}
|
||||
|
||||
return "*";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error handling Geo IP country in request");
|
||||
return "*";
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("GeoIP module starting update task");
|
||||
|
||||
var token = _fileWriteTimeCheckCts.Token;
|
||||
_ = PeriodicReloadTask(token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PeriodicReloadTask(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_processingReload = true;
|
||||
|
||||
var useGeoIP = _lightlessConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.UseGeoIP), false);
|
||||
var cityFile = _lightlessConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.GeoIPDbCityFile), string.Empty);
|
||||
DateTime lastWriteTime = DateTime.MinValue;
|
||||
if (File.Exists(cityFile))
|
||||
{
|
||||
lastWriteTime = new FileInfo(cityFile).LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
if (useGeoIP && (!string.Equals(cityFile, _cityFile, StringComparison.OrdinalIgnoreCase) || lastWriteTime > _dbLastWriteTime))
|
||||
{
|
||||
_cityFile = cityFile;
|
||||
if (!File.Exists(_cityFile)) throw new FileNotFoundException($"Could not open GeoIP City Database, path does not exist: {_cityFile}");
|
||||
_dbReader?.Dispose();
|
||||
_dbReader = null;
|
||||
_dbReader = new DatabaseReader(_cityFile);
|
||||
_dbLastWriteTime = lastWriteTime;
|
||||
|
||||
_ = _dbReader.City("8.8.8.8").Continent;
|
||||
|
||||
_logger.LogInformation($"Loaded GeoIP city file from {_cityFile}");
|
||||
|
||||
if (_useGeoIP != useGeoIP)
|
||||
{
|
||||
_logger.LogInformation("GeoIP module is now enabled");
|
||||
_useGeoIP = useGeoIP;
|
||||
}
|
||||
}
|
||||
|
||||
if (_useGeoIP != useGeoIP && !useGeoIP)
|
||||
{
|
||||
_logger.LogInformation("GeoIP module is now disabled");
|
||||
_useGeoIP = useGeoIP;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Error during periodic GeoIP module reload task, disabling GeoIP");
|
||||
_useGeoIP = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_processingReload = false;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_fileWriteTimeCheckCts.Cancel();
|
||||
_fileWriteTimeCheckCts.Dispose();
|
||||
_dbReader?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Collections.Concurrent;
|
||||
using LightlessSyncAuthService.Authentication;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncAuthService.Services;
|
||||
|
||||
public class SecretKeyAuthenticatorService
|
||||
{
|
||||
private readonly LightlessMetrics _metrics;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
|
||||
private readonly IConfigurationService<AuthServiceConfiguration> _configurationService;
|
||||
private readonly ILogger<SecretKeyAuthenticatorService> _logger;
|
||||
private readonly ConcurrentDictionary<string, SecretKeyFailedAuthorization> _failedAuthorizations = new(StringComparer.Ordinal);
|
||||
|
||||
public SecretKeyAuthenticatorService(LightlessMetrics metrics, IDbContextFactory<LightlessDbContext> dbContextFactory,
|
||||
IConfigurationService<AuthServiceConfiguration> configuration, ILogger<SecretKeyAuthenticatorService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_configurationService = configuration;
|
||||
_metrics = metrics;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<SecretKeyAuthReply> AuthorizeOauthAsync(string ip, string primaryUid, string requestedUid)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests);
|
||||
|
||||
var checkOnIp = FailOnIp(ip);
|
||||
if (checkOnIp != null) return checkOnIp;
|
||||
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
var authUser = await context.Auth.SingleOrDefaultAsync(u => u.UserUID == primaryUid).ConfigureAwait(false);
|
||||
if (authUser == null) return AuthenticationFailure(ip);
|
||||
|
||||
var authReply = await context.Auth.Include(a => a.User).AsNoTracking()
|
||||
.SingleOrDefaultAsync(u => u.UserUID == requestedUid).ConfigureAwait(false);
|
||||
return await GetAuthReply(ip, context, authReply);
|
||||
}
|
||||
|
||||
public async Task<SecretKeyAuthReply> AuthorizeAsync(string ip, string hashedSecretKey)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests);
|
||||
|
||||
var checkOnIp = FailOnIp(ip);
|
||||
if (checkOnIp != null) return checkOnIp;
|
||||
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
var authReply = await context.Auth.Include(a => a.User).AsNoTracking()
|
||||
.SingleOrDefaultAsync(u => u.HashedKey == hashedSecretKey).ConfigureAwait(false);
|
||||
return await GetAuthReply(ip, context, authReply).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<SecretKeyAuthReply> GetAuthReply(string ip, LightlessDbContext context, Auth? authReply)
|
||||
{
|
||||
var isBanned = authReply?.IsBanned ?? false;
|
||||
var markedForBan = authReply?.MarkForBan ?? false;
|
||||
var primaryUid = authReply?.PrimaryUserUID ?? authReply?.UserUID;
|
||||
|
||||
if (authReply?.PrimaryUserUID != null)
|
||||
{
|
||||
var primaryUser = await context.Auth.AsNoTracking().SingleAsync(u => u.UserUID == authReply.PrimaryUserUID).ConfigureAwait(false);
|
||||
isBanned = isBanned || primaryUser.IsBanned;
|
||||
markedForBan = markedForBan || primaryUser.MarkForBan;
|
||||
}
|
||||
|
||||
SecretKeyAuthReply reply = new(authReply != null, authReply?.UserUID,
|
||||
authReply?.PrimaryUserUID ?? authReply?.UserUID, authReply?.User?.Alias ?? string.Empty,
|
||||
TempBan: false, isBanned, markedForBan);
|
||||
|
||||
if (reply.Success)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses);
|
||||
_metrics.IncGauge(MetricsAPI.GaugeAuthenticationCacheEntries);
|
||||
return reply;
|
||||
}
|
||||
else
|
||||
{
|
||||
return AuthenticationFailure(ip);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeyAuthReply? FailOnIp(string ip)
|
||||
{
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization)
|
||||
&& existingFailedAuthorization.FailedAttempts > _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.FailedAuthForTempBan), 5))
|
||||
{
|
||||
if (existingFailedAuthorization.ResetTask == null)
|
||||
{
|
||||
_logger.LogWarning("TempBan {ip} for authorization spam", ip);
|
||||
|
||||
existingFailedAuthorization.ResetTask = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.TempBanDurationInMinutes), 5))).ConfigureAwait(false);
|
||||
|
||||
}).ContinueWith((t) =>
|
||||
{
|
||||
_failedAuthorizations.Remove(ip, out _);
|
||||
});
|
||||
}
|
||||
|
||||
return new(Success: false, Uid: null, PrimaryUid: null, Alias: null, TempBan: true, Permaban: false, MarkedForBan: false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private SecretKeyAuthReply AuthenticationFailure(string ip)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures);
|
||||
|
||||
_logger.LogWarning("Failed authorization from {ip}", ip);
|
||||
var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List<string>());
|
||||
if (!whitelisted.Exists(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var auth))
|
||||
{
|
||||
auth.IncreaseFailedAttempts();
|
||||
}
|
||||
else
|
||||
{
|
||||
_failedAuthorizations[ip] = new SecretKeyFailedAuthorization();
|
||||
}
|
||||
}
|
||||
|
||||
return new(Success: false, Uid: null, PrimaryUid: null, Alias: null, TempBan: false, Permaban: false, MarkedForBan: false);
|
||||
}
|
||||
}
|
||||
244
LightlessSyncServer/LightlessSyncAuthService/Startup.cs
Normal file
244
LightlessSyncServer/LightlessSyncAuthService/Startup.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using LightlessSyncAuthService.Controllers;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using StackExchange.Redis.Extensions.Core.Configuration;
|
||||
using StackExchange.Redis.Extensions.System.Text.Json;
|
||||
using StackExchange.Redis;
|
||||
using System.Net;
|
||||
using LightlessSyncAuthService.Services;
|
||||
using LightlessSyncShared.RequirementHandlers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using StackExchange.Redis.Extensions.Core.Abstractions;
|
||||
|
||||
namespace LightlessSyncAuthService;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private ILogger<Startup> _logger;
|
||||
|
||||
public Startup(IConfiguration configuration, ILogger<Startup> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
|
||||
{
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<LightlessConfigurationBase>>();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseHttpMetrics();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
KestrelMetricServer metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(LightlessConfigurationBase.MetricsPort), 4985));
|
||||
metricServer.Start();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
|
||||
foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>())
|
||||
{
|
||||
if (source == null) continue;
|
||||
_logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
var lightlessConfig = _configuration.GetRequiredSection("LightlessSync");
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
ConfigureRedis(services, lightlessConfig);
|
||||
|
||||
services.AddSingleton<SecretKeyAuthenticatorService>();
|
||||
services.AddSingleton<GeoIPService>();
|
||||
|
||||
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
|
||||
|
||||
services.Configure<AuthServiceConfiguration>(_configuration.GetRequiredSection("LightlessSync"));
|
||||
services.Configure<LightlessConfigurationBase>(_configuration.GetRequiredSection("LightlessSync"));
|
||||
|
||||
services.AddSingleton<ServerTokenGenerator>();
|
||||
|
||||
ConfigureAuthorization(services);
|
||||
|
||||
ConfigureDatabase(services, lightlessConfig);
|
||||
|
||||
ConfigureConfigServices(services);
|
||||
|
||||
ConfigureMetrics(services);
|
||||
|
||||
services.AddHealthChecks();
|
||||
services.AddControllers().ConfigureApplicationPartManager(a =>
|
||||
{
|
||||
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
|
||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController), typeof(OAuthController)));
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureAuthorization(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IAuthorizationHandler, RedisDbUserRequirementHandler>();
|
||||
services.AddTransient<IAuthorizationHandler, ValidTokenRequirementHandler>();
|
||||
services.AddTransient<IAuthorizationHandler, ExistingUserRequirementHandler>();
|
||||
|
||||
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
||||
.Configure<IConfigurationService<LightlessConfigurationBase>>((options, config) =>
|
||||
{
|
||||
options.TokenValidationParameters = new()
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(LightlessConfigurationBase.Jwt)))),
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.DefaultPolicy = new AuthorizationPolicyBuilder()
|
||||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser().Build();
|
||||
options.AddPolicy("OAuthToken", policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
policy.AddRequirements(new ExistingUserRequirement());
|
||||
policy.RequireClaim(LightlessClaimTypes.OAuthLoginToken, "True");
|
||||
});
|
||||
options.AddPolicy("Authenticated", policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
});
|
||||
options.AddPolicy("Identified", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified));
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
|
||||
});
|
||||
options.AddPolicy("Admin", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator));
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
|
||||
});
|
||||
options.AddPolicy("Moderator", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator));
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
});
|
||||
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build());
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureMetrics(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<LightlessMetrics>(m => new LightlessMetrics(m.GetService<ILogger<LightlessMetrics>>(), new List<string>
|
||||
{
|
||||
MetricsAPI.CounterAuthenticationCacheHits,
|
||||
MetricsAPI.CounterAuthenticationFailures,
|
||||
MetricsAPI.CounterAuthenticationRequests,
|
||||
MetricsAPI.CounterAuthenticationSuccesses,
|
||||
}, new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeAuthenticationCacheEntries,
|
||||
}));
|
||||
}
|
||||
|
||||
private void ConfigureRedis(IServiceCollection services, IConfigurationSection lightlessConfig)
|
||||
{
|
||||
// configure redis for SignalR
|
||||
var redisConnection = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
|
||||
var options = ConfigurationOptions.Parse(redisConnection);
|
||||
|
||||
var endpoint = options.EndPoints[0];
|
||||
string address = "";
|
||||
int port = 0;
|
||||
|
||||
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; }
|
||||
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; }
|
||||
/*
|
||||
var redisConfiguration = new RedisConfiguration()
|
||||
{
|
||||
AbortOnConnectFail = true,
|
||||
KeyPrefix = "",
|
||||
Hosts = new RedisHost[]
|
||||
{
|
||||
new RedisHost(){ Host = address, Port = port },
|
||||
},
|
||||
AllowAdmin = true,
|
||||
ConnectTimeout = options.ConnectTimeout,
|
||||
Database = 0,
|
||||
Ssl = false,
|
||||
Password = options.Password,
|
||||
ServerEnumerationStrategy = new ServerEnumerationStrategy()
|
||||
{
|
||||
Mode = ServerEnumerationStrategy.ModeOptions.All,
|
||||
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
|
||||
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw,
|
||||
},
|
||||
MaxValueLength = 1024,
|
||||
PoolSize = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
|
||||
SyncTimeout = options.SyncTimeout,
|
||||
};*/
|
||||
|
||||
var muxer = ConnectionMultiplexer.Connect(options);
|
||||
var db = muxer.GetDatabase();
|
||||
services.AddSingleton<IDatabase>(db);
|
||||
|
||||
_logger.LogInformation("Setting up Redis to connect to {host}:{port}", address, port);
|
||||
}
|
||||
private void ConfigureConfigServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IConfigurationService<AuthServiceConfiguration>, LightlessConfigurationServiceServer<AuthServiceConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceServer<LightlessConfigurationBase>>();
|
||||
}
|
||||
|
||||
private void ConfigureDatabase(IServiceCollection services, IConfigurationSection lightlessConfig)
|
||||
{
|
||||
services.AddDbContextPool<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("LightlessSyncShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, lightlessConfig.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024));
|
||||
services.AddDbContextFactory<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("LightlessSyncShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
65
LightlessSyncServer/LightlessSyncServer.sln
Normal file
65
LightlessSyncServer/LightlessSyncServer.sln
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32602.215
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncServer", "LightlessSyncServer\LightlessSyncServer.csproj", "{029CA97F-E0BA-4172-A191-EA21FB61AD0F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncServerTest", "LightlessSyncServerTest\LightlessSyncServerTest.csproj", "{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncShared", "LightlessSyncShared\LightlessSyncShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncStaticFilesServer", "LightlessSyncStaticFilesServer\LightlessSyncStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncServices", "LightlessSyncServices\LightlessSyncServices.csproj", "{E29C8677-AB44-4950-9EB1-D8E70B710A56}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D5C2B87-5CC9-4FE7-AD13-4C13F6600683}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSyncAuthService", "LightlessSyncAuthService\LightlessSyncAuthService.csproj", "{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync.API", "..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj", "{9A0E2DF9-5E1B-9807-2957-63765888660E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9A0E2DF9-5E1B-9807-2957-63765888660E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9A0E2DF9-5E1B-9807-2957-63765888660E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A0E2DF9-5E1B-9807-2957-63765888660E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A0E2DF9-5E1B-9807-2957-63765888660E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {78C476A5-6E88-449B-828D-A2465D9D3295}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,42 @@
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSyncServer.Hubs;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace LightlessSyncServer.Controllers;
|
||||
|
||||
[Route("/msgc")]
|
||||
[Authorize(Policy = "Internal")]
|
||||
public class ClientMessageController : Controller
|
||||
{
|
||||
private ILogger<ClientMessageController> _logger;
|
||||
private IHubContext<LightlessHub, ILightlessHub> _hubContext;
|
||||
|
||||
public ClientMessageController(ILogger<ClientMessageController> logger, IHubContext<LightlessHub, ILightlessHub> hubContext)
|
||||
{
|
||||
_logger = logger;
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
[Route("sendMessage")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SendMessage(ClientMessage msg)
|
||||
{
|
||||
bool hasUid = !string.IsNullOrEmpty(msg.UID);
|
||||
|
||||
if (!hasUid)
|
||||
{
|
||||
_logger.LogInformation("Sending Message of severity {severity} to all online users: {message}", msg.Severity, msg.Message);
|
||||
await _hubContext.Clients.All.Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Sending Message of severity {severity} to user {uid}: {message}", msg.Severity, msg.UID, msg.Message);
|
||||
await _hubContext.Clients.User(msg.UID).Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public sealed class ConcurrencyFilter : IHubFilter, IDisposable
|
||||
{
|
||||
private ConcurrencyLimiter _limiter;
|
||||
private int _setLimit = 0;
|
||||
private readonly IConfigurationService<ServerConfiguration> _config;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public ConcurrencyFilter(IConfigurationService<ServerConfiguration> config, LightlessMetrics lightlessMetrics)
|
||||
{
|
||||
_config = config;
|
||||
_config.ConfigChangedEvent += OnConfigChange;
|
||||
|
||||
RecreateLimiter();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var token = _cts.Token;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var stats = _limiter?.GetStatistics();
|
||||
if (stats != null)
|
||||
{
|
||||
lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeHubConcurrency, stats.CurrentAvailablePermits);
|
||||
lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeHubQueuedConcurrency, stats.CurrentQueuedCount);
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnConfigChange(object sender, EventArgs e)
|
||||
{
|
||||
RecreateLimiter();
|
||||
}
|
||||
|
||||
private void RecreateLimiter()
|
||||
{
|
||||
var newLimit = _config.GetValueOrDefault(nameof(ServerConfiguration.HubExecutionConcurrencyFilter), 50);
|
||||
|
||||
if (newLimit == _setLimit && _limiter is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_setLimit = newLimit;
|
||||
_limiter?.Dispose();
|
||||
_limiter = new(new ConcurrencyLimiterOptions()
|
||||
{
|
||||
PermitLimit = newLimit,
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = newLimit * 100,
|
||||
});
|
||||
}
|
||||
|
||||
public async ValueTask<object> InvokeMethodAsync(
|
||||
HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
|
||||
{
|
||||
if (string.Equals(invocationContext.HubMethodName, nameof(LightlessHub.CheckClientHealth), StringComparison.Ordinal))
|
||||
{
|
||||
return await next(invocationContext).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var ct = invocationContext.Context.ConnectionAborted;
|
||||
RateLimitLease lease;
|
||||
try
|
||||
{
|
||||
lease = await _limiter.AcquireAsync(1, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
if (!lease.IsAcquired)
|
||||
{
|
||||
throw new HubException("Concurrency limit exceeded. Try again later.");
|
||||
}
|
||||
|
||||
using (lease)
|
||||
{
|
||||
return await next(invocationContext).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_cts.Cancel();
|
||||
_limiter?.Dispose();
|
||||
_config.ConfigChangedEvent -= OnConfigChange;
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSyncServer.Utils;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public partial class LightlessHub
|
||||
{
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataFullDto?> CharaDataCreate()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
int uploadCount = DbContext.CharaData.Count(c => c.UploaderUID == UserUID);
|
||||
User user = DbContext.Users.Single(u => u.UID == UserUID);
|
||||
int maximumUploads = string.IsNullOrEmpty(user.Alias) ? _maxCharaDataByUser : _maxCharaDataByUserVanity;
|
||||
if (uploadCount >= maximumUploads)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string charaDataId = null;
|
||||
while (charaDataId == null)
|
||||
{
|
||||
charaDataId = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFHIJKLMNOPQRSTUVWXYZ");
|
||||
bool idExists = await DbContext.CharaData.AnyAsync(c => c.UploaderUID == UserUID && c.Id == charaDataId).ConfigureAwait(false);
|
||||
if (idExists)
|
||||
{
|
||||
charaDataId = null;
|
||||
}
|
||||
}
|
||||
|
||||
DateTime createdDate = DateTime.UtcNow;
|
||||
CharaData charaData = new()
|
||||
{
|
||||
Id = charaDataId,
|
||||
UploaderUID = UserUID,
|
||||
CreatedDate = createdDate,
|
||||
UpdatedDate = createdDate,
|
||||
AccessType = CharaDataAccess.Individuals,
|
||||
ShareType = CharaDataShare.Private,
|
||||
CustomizeData = string.Empty,
|
||||
GlamourerData = string.Empty,
|
||||
ExpiryDate = DateTime.MaxValue,
|
||||
Description = string.Empty,
|
||||
};
|
||||
|
||||
await DbContext.CharaData.AddAsync(charaData).ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args("SUCCESS", charaDataId));
|
||||
|
||||
return GetCharaDataFullDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> CharaDataDelete(string id)
|
||||
{
|
||||
var existingData = await DbContext.CharaData.SingleOrDefaultAsync(u => u.Id == id && u.UploaderUID == UserUID).ConfigureAwait(false);
|
||||
if (existingData == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args("SUCCESS", id));
|
||||
|
||||
DbContext.Remove(existingData);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args("FAILURE", id, ex.Message));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataDownloadDto?> CharaDataDownload(string id)
|
||||
{
|
||||
CharaData charaData = await GetCharaDataById(id, nameof(CharaDataDownload)).ConfigureAwait(false);
|
||||
|
||||
if (!string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
|
||||
{
|
||||
charaData.DownloadCount++;
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args("SUCCESS", id));
|
||||
|
||||
return GetCharaDataDownloadDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataMetaInfoDto?> CharaDataGetMetainfo(string id)
|
||||
{
|
||||
var charaData = await GetCharaDataById(id, nameof(CharaDataGetMetainfo)).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args("SUCCESS", id));
|
||||
|
||||
return GetCharaDataMetaInfoDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<CharaDataFullDto>> CharaDataGetOwn()
|
||||
{
|
||||
var ownCharaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedUser)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedGroup)
|
||||
.Include(u => u.Poses)
|
||||
.AsSplitQuery()
|
||||
.Where(c => c.UploaderUID == UserUID).ToListAsync().ConfigureAwait(false);
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args("SUCCESS"));
|
||||
return [.. ownCharaData.Select(GetCharaDataFullDto)];
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataFullDto?> CharaDataAttemptRestore(string id)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(id));
|
||||
var charaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedUser)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedGroup)
|
||||
.Include(u => u.Poses)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(s => s.Id == id && s.UploaderUID == UserUID)
|
||||
.ConfigureAwait(false);
|
||||
if (charaData == null)
|
||||
return null;
|
||||
|
||||
var currentHashes = charaData.Files.Select(f => f.FileCacheHash).ToList();
|
||||
var missingFiles = charaData.OriginalFiles.Where(c => !currentHashes.Contains(c.Hash, StringComparer.Ordinal)).ToList();
|
||||
|
||||
// now let's see what's on the db still
|
||||
var existingDbFiles = await DbContext.Files
|
||||
.Where(f => missingFiles.Select(k => k.Hash).Distinct().Contains(f.Hash))
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// now shove it all back into the db
|
||||
foreach (var dbFile in existingDbFiles)
|
||||
{
|
||||
var missingFileEntry = missingFiles.First(f => string.Equals(f.Hash, dbFile.Hash, StringComparison.Ordinal));
|
||||
charaData.Files.Add(new CharaDataFile()
|
||||
{
|
||||
FileCache = dbFile,
|
||||
GamePath = missingFileEntry.GamePath,
|
||||
Parent = charaData
|
||||
});
|
||||
missingFiles.Remove(missingFileEntry);
|
||||
}
|
||||
|
||||
if (existingDbFiles.Any())
|
||||
{
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return GetCharaDataFullDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<CharaDataMetaInfoDto>> CharaDataGetShared()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
List<CharaData> sharedCharaData = [];
|
||||
var groups = await DbContext.GroupPairs
|
||||
.Where(u => u.GroupUserUID == UserUID)
|
||||
.Select(k => k.GroupGID)
|
||||
.AsNoTracking()
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var pairs = (await GetAllPairInfo(UserUID).ConfigureAwait(false));
|
||||
var individualPairs = pairs.Where(p => p.Value.IndividuallyPaired && (!p.Value.OwnPermissions?.IsPaused ?? false) && (!p.Value.OtherPermissions?.IsPaused ?? false)).Select(k => k.Key).ToList();
|
||||
var allPairs = pairs.Where(p => (!p.Value.OwnPermissions?.IsPaused ?? false) && (!p.Value.OtherPermissions?.IsPaused ?? false)).Select(k => k.Key).ToList();
|
||||
|
||||
var allSharedDataByPair = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.Include(u => u.Poses)
|
||||
.Include(u => u.Uploader)
|
||||
.Where(p => p.UploaderUID != UserUID && p.ShareType == CharaDataShare.Shared)
|
||||
.Where(p =>
|
||||
(individualPairs.Contains(p.UploaderUID) && p.AccessType == CharaDataAccess.ClosePairs)
|
||||
|| (allPairs.Contains(p.UploaderUID) && (p.AccessType == CharaDataAccess.AllPairs || p.AccessType == CharaDataAccess.Public))
|
||||
|| (p.AllowedIndividiuals.Any(u => u.AllowedUserUID == UserUID || (u.AllowedGroupGID != null && groups.Contains(u.AllowedGroupGID)))))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
foreach (var charaData in allSharedDataByPair)
|
||||
{
|
||||
sharedCharaData.Add(charaData);
|
||||
}
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args("SUCCESS", sharedCharaData.Count));
|
||||
|
||||
return [.. sharedCharaData.Select(GetCharaDataMetaInfoDto)];
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataFullDto?> CharaDataUpdate(CharaDataUpdateDto updateDto)
|
||||
{
|
||||
var charaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedUser)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedGroup)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.Poses)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(u => u.Id == updateDto.Id && u.UploaderUID == UserUID).ConfigureAwait(false);
|
||||
|
||||
if (charaData == null)
|
||||
return null;
|
||||
|
||||
bool anyChanges = false;
|
||||
|
||||
if (updateDto.Description != null)
|
||||
{
|
||||
charaData.Description = updateDto.Description;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.ExpiryDate != null)
|
||||
{
|
||||
charaData.ExpiryDate = updateDto.ExpiryDate;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.GlamourerData != null)
|
||||
{
|
||||
charaData.GlamourerData = updateDto.GlamourerData;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.CustomizeData != null)
|
||||
{
|
||||
charaData.CustomizeData = updateDto.CustomizeData;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.ManipulationData != null)
|
||||
{
|
||||
charaData.ManipulationData = updateDto.ManipulationData;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.AccessType != null)
|
||||
{
|
||||
charaData.AccessType = GetAccessType(updateDto.AccessType.Value);
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.ShareType != null)
|
||||
{
|
||||
charaData.ShareType = GetShareType(updateDto.ShareType.Value);
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.AllowedUsers != null)
|
||||
{
|
||||
var individuals = charaData.AllowedIndividiuals.Where(k => k.AllowedGroup == null).ToList();
|
||||
var allowedUserList = updateDto.AllowedUsers.ToList();
|
||||
foreach (var user in updateDto.AllowedUsers)
|
||||
{
|
||||
if (charaData.AllowedIndividiuals.Any(k => k.AllowedUser != null && (string.Equals(k.AllowedUser.UID, user, StringComparison.Ordinal) || string.Equals(k.AllowedUser.Alias, user, StringComparison.Ordinal))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == user || u.Alias == user).ConfigureAwait(false);
|
||||
if (dbUser != null)
|
||||
{
|
||||
charaData.AllowedIndividiuals.Add(new CharaDataAllowance()
|
||||
{
|
||||
AllowedUser = dbUser,
|
||||
Parent = charaData
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dataUser in individuals.Where(k => !updateDto.AllowedUsers.Contains(k.AllowedUser.UID, StringComparer.Ordinal) && !updateDto.AllowedUsers.Contains(k.AllowedUser.Alias, StringComparer.Ordinal)))
|
||||
{
|
||||
DbContext.Remove(dataUser);
|
||||
charaData.AllowedIndividiuals.Remove(dataUser);
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.AllowedGroups != null)
|
||||
{
|
||||
var individualGroups = charaData.AllowedIndividiuals.Where(k => k.AllowedUser == null).ToList();
|
||||
var allowedGroups = updateDto.AllowedGroups.ToList();
|
||||
foreach (var group in updateDto.AllowedGroups)
|
||||
{
|
||||
if (charaData.AllowedIndividiuals.Any(k => k.AllowedGroup != null && (string.Equals(k.AllowedGroup.GID, group, StringComparison.Ordinal) || string.Equals(k.AllowedGroup.Alias, group, StringComparison.Ordinal))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupUser = await DbContext.GroupPairs.Include(u => u.Group).SingleOrDefaultAsync(u => (u.Group.GID == group || u.Group.Alias == group) && u.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
if (groupUser != null)
|
||||
{
|
||||
charaData.AllowedIndividiuals.Add(new CharaDataAllowance()
|
||||
{
|
||||
AllowedGroup = groupUser.Group,
|
||||
Parent = charaData
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dataGroup in individualGroups.Where(k => !updateDto.AllowedGroups.Contains(k.AllowedGroup.GID, StringComparer.Ordinal) && !updateDto.AllowedGroups.Contains(k.AllowedGroup.Alias, StringComparer.Ordinal)))
|
||||
{
|
||||
DbContext.Remove(dataGroup);
|
||||
charaData.AllowedIndividiuals.Remove(dataGroup);
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.FileGamePaths != null)
|
||||
{
|
||||
var originalFiles = charaData.OriginalFiles.ToList();
|
||||
charaData.OriginalFiles.Clear();
|
||||
DbContext.RemoveRange(originalFiles);
|
||||
var files = charaData.Files.ToList();
|
||||
charaData.Files.Clear();
|
||||
DbContext.RemoveRange(files);
|
||||
foreach (var file in updateDto.FileGamePaths)
|
||||
{
|
||||
charaData.Files.Add(new CharaDataFile()
|
||||
{
|
||||
FileCacheHash = file.HashOrFileSwap,
|
||||
GamePath = file.GamePath,
|
||||
Parent = charaData
|
||||
});
|
||||
|
||||
charaData.OriginalFiles.Add(new CharaDataOriginalFile()
|
||||
{
|
||||
Hash = file.HashOrFileSwap,
|
||||
Parent = charaData,
|
||||
GamePath = file.GamePath
|
||||
});
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.FileSwaps != null)
|
||||
{
|
||||
var fileSwaps = charaData.FileSwaps.ToList();
|
||||
charaData.FileSwaps.Clear();
|
||||
DbContext.RemoveRange(fileSwaps);
|
||||
foreach (var file in updateDto.FileSwaps)
|
||||
{
|
||||
charaData.FileSwaps.Add(new CharaDataFileSwap()
|
||||
{
|
||||
FilePath = file.HashOrFileSwap,
|
||||
GamePath = file.GamePath,
|
||||
Parent = charaData
|
||||
});
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.Poses != null)
|
||||
{
|
||||
foreach (var pose in updateDto.Poses)
|
||||
{
|
||||
if (pose.Id == null)
|
||||
{
|
||||
charaData.Poses.Add(new CharaDataPose()
|
||||
{
|
||||
Description = pose.Description,
|
||||
Parent = charaData,
|
||||
ParentUploaderUID = UserUID,
|
||||
PoseData = pose.PoseData,
|
||||
WorldData = pose.WorldData == null ? string.Empty : JsonSerializer.Serialize(pose.WorldData),
|
||||
});
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var associatedPose = charaData.Poses.FirstOrDefault(p => p.Id == pose.Id);
|
||||
if (associatedPose == null)
|
||||
continue;
|
||||
|
||||
if (pose.Description == null && pose.PoseData == null && pose.WorldData == null)
|
||||
{
|
||||
charaData.Poses.Remove(associatedPose);
|
||||
DbContext.Remove(associatedPose);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pose.Description != null)
|
||||
associatedPose.Description = pose.Description;
|
||||
if (pose.WorldData != null)
|
||||
{
|
||||
if (pose.WorldData.Value == default) associatedPose.WorldData = string.Empty;
|
||||
else associatedPose.WorldData = JsonSerializer.Serialize(pose.WorldData.Value);
|
||||
}
|
||||
if (pose.PoseData != null)
|
||||
associatedPose.PoseData = pose.PoseData;
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
var overflowingPoses = charaData.Poses.Skip(10).ToList();
|
||||
foreach (var overflowing in overflowingPoses)
|
||||
{
|
||||
charaData.Poses.Remove(overflowing);
|
||||
DbContext.Remove(overflowing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChanges)
|
||||
{
|
||||
charaData.UpdatedDate = DateTime.UtcNow;
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args("SUCCESS", anyChanges));
|
||||
}
|
||||
|
||||
return GetCharaDataFullDto(charaData);
|
||||
}
|
||||
|
||||
private static CharaDataAccess GetAccessType(AccessTypeDto dataAccess) => dataAccess switch
|
||||
{
|
||||
AccessTypeDto.Public => CharaDataAccess.Public,
|
||||
AccessTypeDto.AllPairs => CharaDataAccess.AllPairs,
|
||||
AccessTypeDto.ClosePairs => CharaDataAccess.ClosePairs,
|
||||
AccessTypeDto.Individuals => CharaDataAccess.Individuals,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private static AccessTypeDto GetAccessTypeDto(CharaDataAccess dataAccess) => dataAccess switch
|
||||
{
|
||||
CharaDataAccess.Public => AccessTypeDto.Public,
|
||||
CharaDataAccess.AllPairs => AccessTypeDto.AllPairs,
|
||||
CharaDataAccess.ClosePairs => AccessTypeDto.ClosePairs,
|
||||
CharaDataAccess.Individuals => AccessTypeDto.Individuals,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private static CharaDataDownloadDto GetCharaDataDownloadDto(CharaData charaData)
|
||||
{
|
||||
return new CharaDataDownloadDto(charaData.Id, charaData.Uploader.ToUserData())
|
||||
{
|
||||
CustomizeData = charaData.CustomizeData,
|
||||
Description = charaData.Description,
|
||||
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
|
||||
GlamourerData = charaData.GlamourerData,
|
||||
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
|
||||
ManipulationData = charaData.ManipulationData,
|
||||
};
|
||||
}
|
||||
|
||||
private CharaDataFullDto GetCharaDataFullDto(CharaData charaData)
|
||||
{
|
||||
return new CharaDataFullDto(charaData.Id, new(UserUID))
|
||||
{
|
||||
AccessType = GetAccessTypeDto(charaData.AccessType),
|
||||
ShareType = GetShareTypeDto(charaData.ShareType),
|
||||
AllowedUsers = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedUserUID)).Select(u => new UserData(u.AllowedUser.UID, u.AllowedUser.Alias))],
|
||||
AllowedGroups = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedGroupGID)).Select(k => new GroupData(k.AllowedGroup.GID, k.AllowedGroup.Alias))],
|
||||
CustomizeData = charaData.CustomizeData,
|
||||
Description = charaData.Description,
|
||||
ExpiryDate = charaData.ExpiryDate ?? DateTime.MaxValue,
|
||||
OriginalFiles = charaData.OriginalFiles.Select(k => new GamePathEntry(k.Hash, k.GamePath)).ToList(),
|
||||
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
|
||||
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
|
||||
GlamourerData = charaData.GlamourerData,
|
||||
CreatedDate = charaData.CreatedDate,
|
||||
UpdatedDate = charaData.UpdatedDate,
|
||||
ManipulationData = charaData.ManipulationData,
|
||||
DownloadCount = charaData.DownloadCount,
|
||||
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
|
||||
{
|
||||
WorldData data = default;
|
||||
|
||||
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
|
||||
return new PoseEntry(k.Id)
|
||||
{
|
||||
Description = k.Description,
|
||||
PoseData = k.PoseData,
|
||||
WorldData = data
|
||||
};
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
private static CharaDataMetaInfoDto GetCharaDataMetaInfoDto(CharaData charaData)
|
||||
{
|
||||
var allOrigHashes = charaData.OriginalFiles.Select(k => k.Hash).ToList();
|
||||
var allFileHashes = charaData.Files.Select(f => f.FileCacheHash).ToList();
|
||||
var allHashesPresent = allOrigHashes.TrueForAll(h => allFileHashes.Contains(h, StringComparer.Ordinal));
|
||||
var canBeDownloaded = allHashesPresent &= !string.IsNullOrEmpty(charaData.GlamourerData);
|
||||
return new CharaDataMetaInfoDto(charaData.Id, charaData.Uploader.ToUserData())
|
||||
{
|
||||
CanBeDownloaded = canBeDownloaded,
|
||||
Description = charaData.Description,
|
||||
UpdatedDate = charaData.UpdatedDate,
|
||||
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
|
||||
{
|
||||
WorldData data = default;
|
||||
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
|
||||
return new PoseEntry(k.Id)
|
||||
{
|
||||
Description = k.Description,
|
||||
PoseData = k.PoseData,
|
||||
WorldData = data
|
||||
};
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
private static CharaDataShare GetShareType(ShareTypeDto dataShare) => dataShare switch
|
||||
{
|
||||
ShareTypeDto.Shared => CharaDataShare.Shared,
|
||||
ShareTypeDto.Private => CharaDataShare.Private,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private static ShareTypeDto GetShareTypeDto(CharaDataShare dataShare) => dataShare switch
|
||||
{
|
||||
CharaDataShare.Shared => ShareTypeDto.Shared,
|
||||
CharaDataShare.Private => ShareTypeDto.Private,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private async Task<bool> CheckCharaDataAllowance(CharaData charaData, List<string> joinedGroups)
|
||||
{
|
||||
// check for self
|
||||
if (string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
// check for public access
|
||||
if (charaData.AccessType == CharaDataAccess.Public)
|
||||
return true;
|
||||
|
||||
// check for individuals
|
||||
if (charaData.AllowedIndividiuals.Any(u => string.Equals(u.AllowedUserUID, UserUID, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
if (charaData.AllowedIndividiuals.Any(u => joinedGroups.Contains(u.AllowedGroupGID, StringComparer.Ordinal)))
|
||||
return true;
|
||||
|
||||
var pairInfoUploader = await GetAllPairInfo(charaData.UploaderUID).ConfigureAwait(false);
|
||||
|
||||
// check for all pairs
|
||||
if (charaData.AccessType == CharaDataAccess.AllPairs)
|
||||
{
|
||||
if (pairInfoUploader.TryGetValue(UserUID, out var userInfo) && userInfo.IsSynced && !userInfo.OwnPermissions.IsPaused && !userInfo.OtherPermissions.IsPaused)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for individual pairs
|
||||
if (charaData.AccessType == CharaDataAccess.ClosePairs)
|
||||
{
|
||||
if (pairInfoUploader.TryGetValue(UserUID, out var userInfo) && userInfo.IsSynced && !userInfo.OwnPermissions.IsPaused && !userInfo.OtherPermissions.IsPaused
|
||||
&& userInfo.IndividuallyPaired)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<CharaData> GetCharaDataById(string id, string methodName)
|
||||
{
|
||||
var splitid = id.Split(":", StringSplitOptions.None);
|
||||
if (splitid.Length != 2)
|
||||
{
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args("INVALID", id));
|
||||
throw new InvalidOperationException($"Id {id} not in expected format");
|
||||
}
|
||||
|
||||
var charaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.Include(u => u.Poses)
|
||||
.Include(u => u.Uploader)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(c => c.Id == splitid[1] && c.UploaderUID == splitid[0]).ConfigureAwait(false);
|
||||
|
||||
if (charaData == null)
|
||||
{
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args("NOT FOUND", id));
|
||||
throw new InvalidDataException($"No chara data with {id} found");
|
||||
}
|
||||
|
||||
var groups = await DbContext.GroupPairs.Where(u => u.GroupUserUID == UserUID).Select(k => k.GroupGID).ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!await CheckCharaDataAllowance(charaData, groups).ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args("UNAUTHORIZED", id));
|
||||
throw new UnauthorizedAccessException($"User is not allowed to download {id}");
|
||||
}
|
||||
|
||||
return charaData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
|
||||
namespace LightlessSyncServer.Hubs
|
||||
{
|
||||
public partial class LightlessHub
|
||||
{
|
||||
public Task Client_DownloadReady(Guid requestId) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupDelete(GroupDto groupDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupPairLeft(GroupPairDto groupPairDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupSendInfo(GroupInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserAddClientPair(UserPairDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserReceiveUploadStatus(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserRemoveClientPair(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserSendOffline(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserSendOnline(OnlineUserIdentDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserUpdateProfile(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_UserUpdateDefaultPermissions(DefaultPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyJoin(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyLeave(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
using LightlessSyncShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LightlessSyncServer.Utils;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public partial class LightlessHub
|
||||
{
|
||||
public string UserCharaIdent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.CharaIdent, StringComparison.Ordinal))?.Value ?? throw new Exception("No Chara Ident in Claims");
|
||||
|
||||
public string UserUID => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? throw new Exception("No UID in Claims");
|
||||
|
||||
public string Continent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "UNK";
|
||||
|
||||
private async Task DeleteUser(User user)
|
||||
{
|
||||
var ownPairData = await DbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
var auth = await DbContext.Auth.SingleAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
|
||||
var lodestone = await DbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == user.UID).ConfigureAwait(false);
|
||||
var groupPairs = await DbContext.GroupPairs.Where(g => g.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
var userProfileData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
|
||||
var defaultpermissions = await DbContext.UserDefaultPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
|
||||
var groupPermissions = await DbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
var individualPermissions = await DbContext.Permissions.Where(u => u.UserUID == user.UID || u.OtherUserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
var bannedEntries = await DbContext.GroupBans.Where(u => u.BannedUserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
DbContext.Remove(lodestone);
|
||||
}
|
||||
|
||||
if (userProfileData != null)
|
||||
{
|
||||
DbContext.Remove(userProfileData);
|
||||
}
|
||||
|
||||
while (DbContext.Files.Any(f => f.Uploader == user))
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
DbContext.ClientPairs.RemoveRange(ownPairData);
|
||||
var otherPairData = await DbContext.ClientPairs.Include(u => u.User)
|
||||
.Where(u => u.OtherUser.UID == user.UID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
foreach (var pair in otherPairData)
|
||||
{
|
||||
await Clients.User(pair.UserUID).Client_UserRemoveClientPair(new(user.ToUserData())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var pair in groupPairs)
|
||||
{
|
||||
await UserLeaveGroup(new GroupDto(new GroupData(pair.GroupGID)), user.UID).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (defaultpermissions != null)
|
||||
{
|
||||
DbContext.UserDefaultPreferredPermissions.Remove(defaultpermissions);
|
||||
}
|
||||
DbContext.GroupPairPreferredPermissions.RemoveRange(groupPermissions);
|
||||
DbContext.Permissions.RemoveRange(individualPermissions);
|
||||
DbContext.GroupBans.RemoveRange(bannedEntries);
|
||||
|
||||
_lightlessMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1);
|
||||
|
||||
DbContext.ClientPairs.RemoveRange(otherPairData);
|
||||
DbContext.Users.Remove(user);
|
||||
DbContext.Auth.Remove(auth);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetAllPairedUnpausedUsers(string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
|
||||
return (await GetSyncedUnpausedOnlinePairs(UserUID).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> GetOnlineUsers(List<string> uids)
|
||||
{
|
||||
var result = await _redis.GetAllAsync<string>(uids.Select(u => "UID:" + u).ToHashSet(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
return uids.Where(u => result.TryGetValue("UID:" + u, out var ident) && !string.IsNullOrEmpty(ident)).ToDictionary(u => u, u => result["UID:" + u], StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserIdent(string uid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid)) return string.Empty;
|
||||
return await _redis.GetAsync<string>("UID:" + uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RemoveUserFromRedis()
|
||||
{
|
||||
await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendGroupDeletedToAll(List<GroupPair> groupUsers)
|
||||
{
|
||||
foreach (var pair in groupUsers)
|
||||
{
|
||||
var pairIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(pairIdent)) continue;
|
||||
|
||||
var pairInfo = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupUsers.Where(g => !string.Equals(g.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, pairIdent, pairInfo, pair.GroupUserUID).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<string>> SendOfflineToAllPairedUsers()
|
||||
{
|
||||
var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var self = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
await Clients.Users(usersToSendDataTo).Client_UserSendOffline(new(self.ToUserData())).ConfigureAwait(false);
|
||||
|
||||
return usersToSendDataTo;
|
||||
}
|
||||
|
||||
private async Task<List<string>> SendOnlineToAllPairedUsers()
|
||||
{
|
||||
var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var self = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
await Clients.Users(usersToSendDataTo).Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false);
|
||||
|
||||
return usersToSendDataTo;
|
||||
}
|
||||
|
||||
private async Task<(bool IsValid, Group ReferredGroup)> TryValidateGroupModeratorOrOwner(string gid)
|
||||
{
|
||||
var isOwnerResult = await TryValidateOwner(gid).ConfigureAwait(false);
|
||||
if (isOwnerResult.isValid) return (true, isOwnerResult.ReferredGroup);
|
||||
|
||||
if (isOwnerResult.ReferredGroup == null) return (false, null);
|
||||
|
||||
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
|
||||
|
||||
return (true, isOwnerResult.ReferredGroup);
|
||||
}
|
||||
|
||||
private async Task<(bool isValid, Group ReferredGroup)> TryValidateOwner(string gid)
|
||||
{
|
||||
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null) return (false, null);
|
||||
|
||||
return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group);
|
||||
}
|
||||
|
||||
private async Task<(bool IsValid, GroupPair ReferredPair)> TryValidateUserInGroup(string gid, string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
|
||||
var groupPair = await DbContext.GroupPairs.Include(c => c.GroupUser)
|
||||
.SingleOrDefaultAsync(g => g.GroupGID == gid && (g.GroupUserUID == uid || g.GroupUser.Alias == uid)).ConfigureAwait(false);
|
||||
if (groupPair == null) return (false, null);
|
||||
|
||||
return (true, groupPair);
|
||||
}
|
||||
|
||||
private async Task UpdateUserOnRedis()
|
||||
{
|
||||
await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task UserGroupLeave(GroupPair groupUserPair, string userIdent, Dictionary<string, UserInfo> allUserPairs, string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
if (!allUserPairs.TryGetValue(groupUserPair.GroupUserUID, out var info) || !info.IsSynced)
|
||||
{
|
||||
var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
await Clients.User(uid).Client_UserSendOffline(new(new(groupUserPair.GroupUserUID))).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID).Client_UserSendOffline(new(new(uid))).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UserLeaveGroup(GroupDto dto, string userUid)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (exists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, userUid).ConfigureAwait(false);
|
||||
if (!exists) return;
|
||||
|
||||
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == dto.Group.GID).ConfigureAwait(false);
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToListAsync().ConfigureAwait(false);
|
||||
var groupPairsWithoutSelf = groupPairs.Where(p => !string.Equals(p.GroupUserUID, userUid, StringComparison.Ordinal)).ToList();
|
||||
|
||||
DbContext.GroupPairs.Remove(groupPair);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.User(userUid).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false);
|
||||
|
||||
bool ownerHasLeft = string.Equals(group.OwnerUID, userUid, StringComparison.Ordinal);
|
||||
if (ownerHasLeft)
|
||||
{
|
||||
if (!groupPairsWithoutSelf.Any())
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Deleted"));
|
||||
|
||||
DbContext.Groups.Remove(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(DbContext, group, groupPairsWithoutSelf, _maxExistingGroupsByUser).ConfigureAwait(false);
|
||||
|
||||
if (groupHasMigrated.Item1)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Migrated", groupHasMigrated.Item2));
|
||||
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == groupHasMigrated.Item2).ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(),
|
||||
user.ToUserData(), group.ToEnum())).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Deleted"));
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupDelete(dto).ConfigureAwait(false);
|
||||
|
||||
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == userUid).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupPairLeft(new GroupPairDto(dto.Group, groupPair.GroupUser.ToUserData())).ConfigureAwait(false);
|
||||
|
||||
var ident = await GetUserIdent(userUid).ConfigureAwait(false);
|
||||
|
||||
var pairs = await GetAllPairInfo(userUid).ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupPairsWithoutSelf)
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, ident, pairs, userUid).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<UserInfo?> GetPairInfo(string uid, string otheruid)
|
||||
{
|
||||
var clientPairs = from cp in DbContext.ClientPairs.AsNoTracking().Where(u => u.UserUID == uid && u.OtherUserUID == otheruid)
|
||||
join cp2 in DbContext.ClientPairs.AsNoTracking().Where(u => u.OtherUserUID == uid && u.UserUID == otheruid)
|
||||
on new
|
||||
{
|
||||
UserUID = cp.UserUID,
|
||||
OtherUserUID = cp.OtherUserUID
|
||||
}
|
||||
equals new
|
||||
{
|
||||
UserUID = cp2.OtherUserUID,
|
||||
OtherUserUID = cp2.UserUID
|
||||
} into joined
|
||||
from c in joined.DefaultIfEmpty()
|
||||
where cp.UserUID == uid
|
||||
select new
|
||||
{
|
||||
UserUID = cp.UserUID,
|
||||
OtherUserUID = cp.OtherUserUID,
|
||||
Gid = string.Empty,
|
||||
Synced = c != null
|
||||
};
|
||||
|
||||
|
||||
var groupPairs = from gp in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == uid)
|
||||
join gp2 in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == otheruid)
|
||||
on new
|
||||
{
|
||||
GID = gp.GroupGID
|
||||
}
|
||||
equals new
|
||||
{
|
||||
GID = gp2.GroupGID
|
||||
}
|
||||
where gp.GroupUserUID == uid
|
||||
select new
|
||||
{
|
||||
UserUID = gp.GroupUserUID,
|
||||
OtherUserUID = gp2.GroupUserUID,
|
||||
Gid = Convert.ToString(gp2.GroupGID),
|
||||
Synced = true
|
||||
};
|
||||
|
||||
var allPairs = clientPairs.Concat(groupPairs);
|
||||
|
||||
var result = from user in allPairs
|
||||
join u in DbContext.Users.AsNoTracking() on user.OtherUserUID equals u.UID
|
||||
join o in DbContext.Permissions.AsNoTracking().Where(u => u.UserUID == uid)
|
||||
on new { UserUID = user.UserUID, OtherUserUID = user.OtherUserUID }
|
||||
equals new { UserUID = o.UserUID, OtherUserUID = o.OtherUserUID }
|
||||
into ownperms
|
||||
from ownperm in ownperms.DefaultIfEmpty()
|
||||
join p in DbContext.Permissions.AsNoTracking().Where(u => u.OtherUserUID == uid)
|
||||
on new { UserUID = user.OtherUserUID, OtherUserUID = user.UserUID }
|
||||
equals new { UserUID = p.UserUID, OtherUserUID = p.OtherUserUID }
|
||||
into otherperms
|
||||
from otherperm in otherperms.DefaultIfEmpty()
|
||||
where user.UserUID == uid
|
||||
&& u.UID == user.OtherUserUID
|
||||
&& ownperm.UserUID == user.UserUID && ownperm.OtherUserUID == user.OtherUserUID
|
||||
&& (otherperm == null || (otherperm.OtherUserUID == user.UserUID && otherperm.UserUID == user.OtherUserUID))
|
||||
select new
|
||||
{
|
||||
UserUID = user.UserUID,
|
||||
OtherUserUID = user.OtherUserUID,
|
||||
OtherUserAlias = u.Alias,
|
||||
GID = user.Gid,
|
||||
Synced = user.Synced,
|
||||
OwnPermissions = ownperm,
|
||||
OtherPermissions = otherperm
|
||||
};
|
||||
|
||||
var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
if (!resultList.Any()) return null;
|
||||
|
||||
var groups = resultList.Select(g => g.GID).ToList();
|
||||
return new UserInfo(resultList[0].OtherUserAlias,
|
||||
resultList.SingleOrDefault(p => string.IsNullOrEmpty(p.GID))?.Synced ?? false,
|
||||
resultList.Max(p => p.Synced),
|
||||
resultList.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(),
|
||||
resultList[0].OwnPermissions,
|
||||
resultList[0].OtherPermissions);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, UserInfo>> GetAllPairInfo(string uid)
|
||||
{
|
||||
var clientPairs = from cp in DbContext.ClientPairs.AsNoTracking().Where(u => u.UserUID == uid)
|
||||
join cp2 in DbContext.ClientPairs.AsNoTracking().Where(u => u.OtherUserUID == uid)
|
||||
on new
|
||||
{
|
||||
UserUID = cp.UserUID,
|
||||
OtherUserUID = cp.OtherUserUID
|
||||
}
|
||||
equals new
|
||||
{
|
||||
UserUID = cp2.OtherUserUID,
|
||||
OtherUserUID = cp2.UserUID
|
||||
} into joined
|
||||
from c in joined.DefaultIfEmpty()
|
||||
where cp.UserUID == uid
|
||||
select new
|
||||
{
|
||||
UserUID = cp.UserUID,
|
||||
OtherUserUID = cp.OtherUserUID,
|
||||
Gid = string.Empty,
|
||||
Synced = c != null
|
||||
};
|
||||
|
||||
|
||||
var groupPairs = from gp in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == uid)
|
||||
join gp2 in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID != uid)
|
||||
on new
|
||||
{
|
||||
GID = gp.GroupGID
|
||||
}
|
||||
equals new
|
||||
{
|
||||
GID = gp2.GroupGID
|
||||
}
|
||||
select new
|
||||
{
|
||||
UserUID = gp.GroupUserUID,
|
||||
OtherUserUID = gp2.GroupUserUID,
|
||||
Gid = Convert.ToString(gp2.GroupGID),
|
||||
Synced = true
|
||||
};
|
||||
|
||||
var allPairs = clientPairs.Concat(groupPairs);
|
||||
|
||||
var result = from user in allPairs
|
||||
join u in DbContext.Users.AsNoTracking() on user.OtherUserUID equals u.UID
|
||||
join o in DbContext.Permissions.AsNoTracking().Where(u => u.UserUID == uid)
|
||||
on new { UserUID = user.UserUID, OtherUserUID = user.OtherUserUID }
|
||||
equals new { UserUID = o.UserUID, OtherUserUID = o.OtherUserUID }
|
||||
into ownperms
|
||||
from ownperm in ownperms.DefaultIfEmpty()
|
||||
join p in DbContext.Permissions.AsNoTracking().Where(u => u.OtherUserUID == uid)
|
||||
on new { UserUID = user.OtherUserUID, OtherUserUID = user.UserUID }
|
||||
equals new { UserUID = p.UserUID, OtherUserUID = p.OtherUserUID }
|
||||
into otherperms
|
||||
from otherperm in otherperms.DefaultIfEmpty()
|
||||
where user.UserUID == uid
|
||||
&& u.UID == user.OtherUserUID
|
||||
&& ownperm.UserUID == user.UserUID && ownperm.OtherUserUID == user.OtherUserUID
|
||||
&& (otherperm == null || (otherperm.OtherUserUID == user.UserUID && otherperm.UserUID == user.OtherUserUID))
|
||||
select new
|
||||
{
|
||||
UserUID = user.UserUID,
|
||||
OtherUserUID = user.OtherUserUID,
|
||||
OtherUserAlias = u.Alias,
|
||||
GID = user.Gid,
|
||||
Synced = user.Synced,
|
||||
OwnPermissions = ownperm,
|
||||
OtherPermissions = otherperm
|
||||
};
|
||||
|
||||
var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
return resultList.GroupBy(g => g.OtherUserUID, StringComparer.Ordinal).ToDictionary(g => g.Key, g =>
|
||||
{
|
||||
return new UserInfo(g.First().OtherUserAlias,
|
||||
g.SingleOrDefault(p => string.IsNullOrEmpty(p.GID))?.Synced ?? false,
|
||||
g.Max(p => p.Synced),
|
||||
g.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(),
|
||||
g.First().OwnPermissions,
|
||||
g.First().OtherPermissions);
|
||||
}, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetSyncedUnpausedOnlinePairs(string uid)
|
||||
{
|
||||
var clientPairs = from cp in DbContext.ClientPairs.AsNoTracking().Where(u => u.UserUID == uid)
|
||||
join cp2 in DbContext.ClientPairs.AsNoTracking().Where(u => u.OtherUserUID == uid)
|
||||
on new
|
||||
{
|
||||
UserUID = cp.UserUID,
|
||||
OtherUserUID = cp.OtherUserUID
|
||||
}
|
||||
equals new
|
||||
{
|
||||
UserUID = cp2.OtherUserUID,
|
||||
OtherUserUID = cp2.UserUID
|
||||
} into joined
|
||||
from c in joined.DefaultIfEmpty()
|
||||
where cp.UserUID == uid && c.UserUID != null
|
||||
select new
|
||||
{
|
||||
UserUID = cp.UserUID,
|
||||
OtherUserUID = cp.OtherUserUID,
|
||||
};
|
||||
|
||||
|
||||
var groupPairs = from gp in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == uid)
|
||||
join gp2 in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID != uid)
|
||||
on new
|
||||
{
|
||||
GID = gp.GroupGID
|
||||
}
|
||||
equals new
|
||||
{
|
||||
GID = gp2.GroupGID
|
||||
}
|
||||
select new
|
||||
{
|
||||
UserUID = gp.GroupUserUID,
|
||||
OtherUserUID = gp2.GroupUserUID,
|
||||
};
|
||||
|
||||
var allPairs = clientPairs.Concat(groupPairs);
|
||||
|
||||
var result = from user in allPairs
|
||||
join o in DbContext.Permissions.AsNoTracking().Where(u => u.UserUID == uid)
|
||||
on new { UserUID = user.UserUID, OtherUserUID = user.OtherUserUID }
|
||||
equals new { UserUID = o.UserUID, OtherUserUID = o.OtherUserUID }
|
||||
into ownperms
|
||||
from ownperm in ownperms.DefaultIfEmpty()
|
||||
join p in DbContext.Permissions.AsNoTracking().Where(u => u.OtherUserUID == uid)
|
||||
on new { UserUID = user.OtherUserUID, OtherUserUID = user.UserUID }
|
||||
equals new { UserUID = p.UserUID, OtherUserUID = p.OtherUserUID }
|
||||
into otherperms
|
||||
from otherperm in otherperms.DefaultIfEmpty()
|
||||
where user.UserUID == uid
|
||||
&& ownperm.UserUID == user.UserUID && ownperm.OtherUserUID == user.OtherUserUID
|
||||
&& otherperm.OtherUserUID == user.UserUID && otherperm.UserUID == user.OtherUserUID
|
||||
&& !ownperm.IsPaused && (otherperm == null ? false : !otherperm.IsPaused)
|
||||
select user.OtherUserUID;
|
||||
|
||||
return await result.Distinct().AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public record UserInfo(string Alias, bool IndividuallyPaired, bool IsSynced, List<string> GIDs, UserPermissionSet? OwnPermissions, UserPermissionSet? OtherPermissions);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSyncServer.Utils;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public partial class LightlessHub
|
||||
{
|
||||
private async Task<string?> GetUserGposeLobby()
|
||||
{
|
||||
return await _redis.GetAsync<string>(GposeLobbyUser).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetUsersInLobby(string lobbyId, bool includeSelf = false)
|
||||
{
|
||||
var users = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}").ConfigureAwait(false);
|
||||
return users?.Where(u => includeSelf || !string.Equals(u, UserUID, StringComparison.Ordinal)).ToList() ?? [];
|
||||
}
|
||||
|
||||
private async Task AddUserToLobby(string lobbyId, List<string> priorUsers)
|
||||
{
|
||||
_lightlessMetrics.IncGauge(MetricsAPI.GaugeGposeLobbyUsers);
|
||||
if (priorUsers.Count == 0)
|
||||
_lightlessMetrics.IncGauge(MetricsAPI.GaugeGposeLobbies);
|
||||
|
||||
await _redis.AddAsync(GposeLobbyUser, lobbyId).ConfigureAwait(false);
|
||||
await _redis.AddAsync($"GposeLobby:{lobbyId}", priorUsers.Concat([UserUID])).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RemoveUserFromLobby(string lobbyId, List<string> priorUsers)
|
||||
{
|
||||
await _redis.RemoveAsync(GposeLobbyUser).ConfigureAwait(false);
|
||||
|
||||
_lightlessMetrics.DecGauge(MetricsAPI.GaugeGposeLobbyUsers);
|
||||
|
||||
if (priorUsers.Count == 1)
|
||||
{
|
||||
await _redis.RemoveAsync($"GposeLobby:{lobbyId}").ConfigureAwait(false);
|
||||
_lightlessMetrics.DecGauge(MetricsAPI.GaugeGposeLobbies);
|
||||
}
|
||||
else
|
||||
{
|
||||
priorUsers.Remove(UserUID);
|
||||
await _redis.AddAsync($"GposeLobby:{lobbyId}", priorUsers).ConfigureAwait(false);
|
||||
await Clients.Users(priorUsers).Client_GposeLobbyLeave(new(UserUID)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private string GposeLobbyUser => $"GposeLobbyUser:{UserUID}";
|
||||
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<string> GposeLobbyCreate()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var alreadyInLobby = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(alreadyInLobby))
|
||||
{
|
||||
throw new HubException("Already in GPose Lobby, cannot join another");
|
||||
}
|
||||
|
||||
string lobbyId = string.Empty;
|
||||
while (string.IsNullOrEmpty(lobbyId))
|
||||
{
|
||||
lobbyId = StringUtils.GenerateRandomString(30, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
|
||||
var result = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}").ConfigureAwait(false);
|
||||
if (result != null)
|
||||
lobbyId = string.Empty;
|
||||
}
|
||||
|
||||
await AddUserToLobby(lobbyId, []).ConfigureAwait(false);
|
||||
|
||||
return lobbyId;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<UserData>> GposeLobbyJoin(string lobbyId)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var existingLobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(existingLobbyId))
|
||||
await GposeLobbyLeave().ConfigureAwait(false);
|
||||
|
||||
var lobbyUsers = await GetUsersInLobby(lobbyId).ConfigureAwait(false);
|
||||
if (!lobbyUsers.Any())
|
||||
return [];
|
||||
|
||||
await AddUserToLobby(lobbyId, lobbyUsers).ConfigureAwait(false);
|
||||
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
await Clients.Users(lobbyUsers.Where(u => !string.Equals(u, UserUID, StringComparison.Ordinal)))
|
||||
.Client_GposeLobbyJoin(user.ToUserData()).ConfigureAwait(false);
|
||||
|
||||
var users = await DbContext.Users.Where(u => lobbyUsers.Contains(u.UID))
|
||||
.Select(u => u.ToUserData())
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> GposeLobbyLeave()
|
||||
{
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return true;
|
||||
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var lobbyUsers = await GetUsersInLobby(lobbyId, true).ConfigureAwait(false);
|
||||
await RemoveUserFromLobby(lobbyId, lobbyUsers).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDataDownloadDto)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return;
|
||||
|
||||
var lobbyUsers = await GetUsersInLobby(lobbyId).ConfigureAwait(false);
|
||||
await Clients.Users(lobbyUsers).Client_GposeLobbyPushCharacterData(charaDataDownloadDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GposeLobbyPushPoseData(PoseData poseData)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return;
|
||||
|
||||
await _gPoseLobbyDistributionService.PushPoseData(lobbyId, UserUID, poseData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GposeLobbyPushWorldData(WorldData worldData)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return;
|
||||
|
||||
await _gPoseLobbyDistributionService.PushWorldData(lobbyId, UserUID, worldData).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
672
LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs
Normal file
672
LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs
Normal file
@@ -0,0 +1,672 @@
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSyncServer.Utils;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public partial class LightlessHub
|
||||
{
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupBanUser(GroupPairDto dto, string reason)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, reason));
|
||||
|
||||
var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!userHasRights) return;
|
||||
|
||||
var (userExists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!userExists) return;
|
||||
|
||||
if (groupPair.IsModerator || string.Equals(group.OwnerUID, dto.User.UID, StringComparison.Ordinal)) return;
|
||||
|
||||
var alias = string.IsNullOrEmpty(groupPair.GroupUser.Alias) ? "-" : groupPair.GroupUser.Alias;
|
||||
var ban = new GroupBan()
|
||||
{
|
||||
BannedByUID = UserUID,
|
||||
BannedReason = $"{reason} (Alias at time of ban: {alias})",
|
||||
BannedOn = DateTime.UtcNow,
|
||||
BannedUserUID = dto.User.UID,
|
||||
GroupGID = dto.Group.GID,
|
||||
};
|
||||
|
||||
DbContext.Add(ban);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await GroupRemoveUser(dto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return;
|
||||
|
||||
group.InvitesEnabled = !dto.Permissions.HasFlag(GroupPermissions.DisableInvites);
|
||||
group.PreferDisableSounds = dto.Permissions.HasFlag(GroupPermissions.PreferDisableSounds);
|
||||
group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations);
|
||||
group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX);
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList();
|
||||
await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupChangeOwnership(GroupPairDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (isOwner, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!isOwner) return;
|
||||
|
||||
var (isInGroup, newOwnerPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!isInGroup) return;
|
||||
|
||||
var ownedShells = await DbContext.Groups.CountAsync(g => g.OwnerUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (ownedShells >= _maxExistingGroupsByUser) return;
|
||||
|
||||
var prevOwner = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
prevOwner.IsPinned = false;
|
||||
group.Owner = newOwnerPair.GroupUser;
|
||||
group.Alias = null;
|
||||
newOwnerPair.IsPinned = true;
|
||||
newOwnerPair.IsModerator = false;
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), newOwnerPair.GroupUser.ToUserData(), group.ToEnum())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> GroupChangePassword(GroupPasswordDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (isOwner, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!isOwner || dto.Password.Length < 10) return false;
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
group.HashedPassword = StringUtils.Sha256String(dto.Password);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupClear(GroupDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return;
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false);
|
||||
var notPinned = groupPairs.Where(g => !g.IsPinned && !g.IsModerator).ToList();
|
||||
|
||||
await Clients.Users(notPinned.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
DbContext.GroupPairs.RemoveRange(notPinned);
|
||||
|
||||
foreach (var pair in notPinned)
|
||||
{
|
||||
await Clients.Users(groupPairs.Where(p => p.IsPinned || p.IsModerator).Select(g => g.GroupUserUID))
|
||||
.Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false);
|
||||
|
||||
var pairIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(pairIdent)) continue;
|
||||
|
||||
var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false);
|
||||
|
||||
var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||
|
||||
foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
|
||||
{
|
||||
await UserGroupLeave(pair, pairIdent, allUserPairs, pair.GroupUserUID).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<GroupJoinDto> GroupCreate()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false);
|
||||
var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser)
|
||||
{
|
||||
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
|
||||
}
|
||||
|
||||
var gid = StringUtils.GenerateRandomString(12);
|
||||
while (await DbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid).ConfigureAwait(false))
|
||||
{
|
||||
gid = StringUtils.GenerateRandomString(12);
|
||||
}
|
||||
gid = "MSS-" + gid;
|
||||
|
||||
var passwd = StringUtils.GenerateRandomString(16);
|
||||
using var sha = SHA256.Create();
|
||||
var hashedPw = StringUtils.Sha256String(passwd);
|
||||
|
||||
UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false);
|
||||
|
||||
Group newGroup = new()
|
||||
{
|
||||
GID = gid,
|
||||
HashedPassword = hashedPw,
|
||||
InvitesEnabled = true,
|
||||
OwnerUID = UserUID,
|
||||
PreferDisableAnimations = defaultPermissions.DisableGroupAnimations,
|
||||
PreferDisableSounds = defaultPermissions.DisableGroupSounds,
|
||||
PreferDisableVFX = defaultPermissions.DisableGroupVFX
|
||||
};
|
||||
|
||||
GroupPair initialPair = new()
|
||||
{
|
||||
GroupGID = newGroup.GID,
|
||||
GroupUserUID = UserUID,
|
||||
IsPinned = true,
|
||||
};
|
||||
|
||||
GroupPairPreferredPermission initialPrefPermissions = new()
|
||||
{
|
||||
UserUID = UserUID,
|
||||
GroupGID = newGroup.GID,
|
||||
DisableSounds = defaultPermissions.DisableGroupSounds,
|
||||
DisableAnimations = defaultPermissions.DisableGroupAnimations,
|
||||
DisableVFX = defaultPermissions.DisableGroupAnimations
|
||||
};
|
||||
|
||||
await DbContext.Groups.AddAsync(newGroup).ConfigureAwait(false);
|
||||
await DbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false);
|
||||
await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions).ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
|
||||
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(),
|
||||
newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal)))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(gid));
|
||||
|
||||
return new GroupJoinDto(newGroup.ToGroupData(), passwd, initialPrefPermissions.ToEnum());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<string>> GroupCreateTempInvite(GroupDto dto, int amount)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, amount));
|
||||
List<string> inviteCodes = new();
|
||||
List<GroupTempInvite> tempInvites = new();
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return new();
|
||||
|
||||
var existingInvites = await DbContext.GroupTempInvites.Where(g => g.GroupGID == group.GID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
bool hasValidInvite = false;
|
||||
string invite = string.Empty;
|
||||
string hashedInvite = string.Empty;
|
||||
while (!hasValidInvite)
|
||||
{
|
||||
invite = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
|
||||
hashedInvite = StringUtils.Sha256String(invite);
|
||||
if (existingInvites.Any(i => string.Equals(i.Invite, hashedInvite, StringComparison.Ordinal))) continue;
|
||||
hasValidInvite = true;
|
||||
inviteCodes.Add(invite);
|
||||
}
|
||||
|
||||
tempInvites.Add(new GroupTempInvite()
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.AddDays(1),
|
||||
GroupGID = group.GID,
|
||||
Invite = hashedInvite,
|
||||
});
|
||||
}
|
||||
|
||||
DbContext.GroupTempInvites.AddRange(tempInvites);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return inviteCodes;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupDelete(GroupDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.RemoveRange(groupPairs);
|
||||
DbContext.Remove(group);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false);
|
||||
|
||||
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<BannedGroupUserDto>> GroupGetBannedUsers(GroupDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
|
||||
if (!userHasRights) return new List<BannedGroupUserDto>();
|
||||
|
||||
var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
List<BannedGroupUserDto> bannedGroupUsers = banEntries.Select(b =>
|
||||
new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn,
|
||||
b.BannedByUID)).ToList();
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, bannedGroupUsers.Count));
|
||||
|
||||
return bannedGroupUsers;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<GroupJoinInfoDto> GroupJoin(GroupPasswordDto dto)
|
||||
{
|
||||
var aliasOrGid = dto.Group.GID.Trim();
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false);
|
||||
var groupGid = group?.GID ?? string.Empty;
|
||||
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
var hashedPw = StringUtils.Sha256String(dto.Password);
|
||||
var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false);
|
||||
var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false);
|
||||
var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false);
|
||||
|
||||
if (group == null
|
||||
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null)
|
||||
|| existingPair != null
|
||||
|| existingUserCount >= _maxGroupUserCount
|
||||
|| !group.InvitesEnabled
|
||||
|| joinedGroups >= _maxJoinedGroupsByUser
|
||||
|| isBanned)
|
||||
return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false);
|
||||
|
||||
return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> GroupJoinFinalize(GroupJoinDto dto)
|
||||
{
|
||||
var aliasOrGid = dto.Group.GID.Trim();
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false);
|
||||
var groupGid = group?.GID ?? string.Empty;
|
||||
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
var hashedPw = StringUtils.Sha256String(dto.Password);
|
||||
var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false);
|
||||
var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false);
|
||||
var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false);
|
||||
|
||||
if (group == null
|
||||
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null)
|
||||
|| existingPair != null
|
||||
|| existingUserCount >= _maxGroupUserCount
|
||||
|| !group.InvitesEnabled
|
||||
|| joinedGroups >= _maxJoinedGroupsByUser
|
||||
|| isBanned)
|
||||
return false;
|
||||
|
||||
// get all pairs before we join
|
||||
var allUserPairs = (await GetAllPairInfo(UserUID).ConfigureAwait(false));
|
||||
|
||||
if (oneTimeInvite != null)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(aliasOrGid, "TempInvite", oneTimeInvite.Invite));
|
||||
DbContext.Remove(oneTimeInvite);
|
||||
}
|
||||
|
||||
GroupPair newPair = new()
|
||||
{
|
||||
GroupGID = group.GID,
|
||||
GroupUserUID = UserUID,
|
||||
};
|
||||
|
||||
var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID).ConfigureAwait(false);
|
||||
if (preferredPermissions == null)
|
||||
{
|
||||
GroupPairPreferredPermission newPerms = new()
|
||||
{
|
||||
GroupGID = group.GID,
|
||||
UserUID = UserUID,
|
||||
DisableSounds = dto.GroupUserPreferredPermissions.IsDisableSounds(),
|
||||
DisableVFX = dto.GroupUserPreferredPermissions.IsDisableVFX(),
|
||||
DisableAnimations = dto.GroupUserPreferredPermissions.IsDisableAnimations(),
|
||||
IsPaused = false
|
||||
};
|
||||
|
||||
DbContext.Add(newPerms);
|
||||
preferredPermissions = newPerms;
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredPermissions.DisableSounds = dto.GroupUserPreferredPermissions.IsDisableSounds();
|
||||
preferredPermissions.DisableVFX = dto.GroupUserPreferredPermissions.IsDisableVFX();
|
||||
preferredPermissions.DisableAnimations = dto.GroupUserPreferredPermissions.IsDisableAnimations();
|
||||
preferredPermissions.IsPaused = false;
|
||||
DbContext.Update(preferredPermissions);
|
||||
}
|
||||
|
||||
await DbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(aliasOrGid, "Success"));
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync().ConfigureAwait(false);
|
||||
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(),
|
||||
group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(),
|
||||
groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false);
|
||||
|
||||
var self = DbContext.Users.Single(u => u.UID == UserUID);
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser)
|
||||
.Where(p => p.GroupGID == group.GID && p.GroupUserUID != UserUID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
var userPairsAfterJoin = await GetAllPairInfo(UserUID).ConfigureAwait(false);
|
||||
|
||||
foreach (var pair in groupPairs)
|
||||
{
|
||||
var perms = userPairsAfterJoin.TryGetValue(pair.GroupUserUID, out var userinfo);
|
||||
// check if we have had prior permissions to that pair, if not add them
|
||||
var ownPermissionsToOther = userinfo?.OwnPermissions ?? null;
|
||||
if (ownPermissionsToOther == null)
|
||||
{
|
||||
var existingPermissionsOnDb = await DbContext.Permissions.SingleOrDefaultAsync(p => p.UserUID == UserUID && p.OtherUserUID == pair.GroupUserUID).ConfigureAwait(false);
|
||||
|
||||
if (existingPermissionsOnDb == null)
|
||||
{
|
||||
ownPermissionsToOther = new()
|
||||
{
|
||||
UserUID = UserUID,
|
||||
OtherUserUID = pair.GroupUserUID,
|
||||
DisableAnimations = preferredPermissions.DisableAnimations,
|
||||
DisableSounds = preferredPermissions.DisableSounds,
|
||||
DisableVFX = preferredPermissions.DisableVFX,
|
||||
IsPaused = preferredPermissions.IsPaused,
|
||||
Sticky = false
|
||||
};
|
||||
|
||||
await DbContext.Permissions.AddAsync(ownPermissionsToOther).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingPermissionsOnDb.DisableAnimations = preferredPermissions.DisableAnimations;
|
||||
existingPermissionsOnDb.DisableSounds = preferredPermissions.DisableSounds;
|
||||
existingPermissionsOnDb.DisableVFX = preferredPermissions.DisableVFX;
|
||||
existingPermissionsOnDb.IsPaused = false;
|
||||
existingPermissionsOnDb.Sticky = false;
|
||||
|
||||
DbContext.Update(existingPermissionsOnDb);
|
||||
|
||||
ownPermissionsToOther = existingPermissionsOnDb;
|
||||
}
|
||||
}
|
||||
else if (!ownPermissionsToOther.Sticky)
|
||||
{
|
||||
ownPermissionsToOther = await DbContext.Permissions.SingleAsync(u => u.UserUID == UserUID && u.OtherUserUID == pair.GroupUserUID).ConfigureAwait(false);
|
||||
|
||||
// update the existing permission only if it was not set to sticky
|
||||
ownPermissionsToOther.DisableAnimations = preferredPermissions.DisableAnimations;
|
||||
ownPermissionsToOther.DisableVFX = preferredPermissions.DisableVFX;
|
||||
ownPermissionsToOther.DisableSounds = preferredPermissions.DisableSounds;
|
||||
ownPermissionsToOther.IsPaused = false;
|
||||
|
||||
DbContext.Update(ownPermissionsToOther);
|
||||
}
|
||||
|
||||
// get others permissionset to self and eventually update it
|
||||
var otherPermissionToSelf = userinfo?.OtherPermissions ?? null;
|
||||
if (otherPermissionToSelf == null)
|
||||
{
|
||||
var otherExistingPermsOnDb = await DbContext.Permissions.SingleOrDefaultAsync(p => p.UserUID == pair.GroupUserUID && p.OtherUserUID == UserUID).ConfigureAwait(false);
|
||||
|
||||
if (otherExistingPermsOnDb == null)
|
||||
{
|
||||
var otherPreferred = await DbContext.GroupPairPreferredPermissions.SingleAsync(u => u.GroupGID == group.GID && u.UserUID == pair.GroupUserUID).ConfigureAwait(false);
|
||||
otherExistingPermsOnDb = new()
|
||||
{
|
||||
UserUID = pair.GroupUserUID,
|
||||
OtherUserUID = UserUID,
|
||||
DisableAnimations = otherPreferred.DisableAnimations,
|
||||
DisableSounds = otherPreferred.DisableSounds,
|
||||
DisableVFX = otherPreferred.DisableVFX,
|
||||
IsPaused = otherPreferred.IsPaused,
|
||||
Sticky = false
|
||||
};
|
||||
|
||||
await DbContext.AddAsync(otherExistingPermsOnDb).ConfigureAwait(false);
|
||||
}
|
||||
else if (!otherExistingPermsOnDb.Sticky)
|
||||
{
|
||||
var otherPreferred = await DbContext.GroupPairPreferredPermissions.SingleAsync(u => u.GroupGID == group.GID && u.UserUID == pair.GroupUserUID).ConfigureAwait(false);
|
||||
otherExistingPermsOnDb.DisableAnimations = otherPreferred.DisableAnimations;
|
||||
otherExistingPermsOnDb.DisableSounds = otherPreferred.DisableSounds;
|
||||
otherExistingPermsOnDb.DisableVFX = otherPreferred.DisableVFX;
|
||||
otherExistingPermsOnDb.IsPaused = otherPreferred.IsPaused;
|
||||
|
||||
DbContext.Update(otherExistingPermsOnDb);
|
||||
}
|
||||
|
||||
otherPermissionToSelf = otherExistingPermsOnDb;
|
||||
}
|
||||
else if (!otherPermissionToSelf.Sticky)
|
||||
{
|
||||
var otherPreferred = await DbContext.GroupPairPreferredPermissions.SingleAsync(u => u.GroupGID == group.GID && u.UserUID == pair.GroupUserUID).ConfigureAwait(false);
|
||||
otherPermissionToSelf.DisableAnimations = otherPreferred.DisableAnimations;
|
||||
otherPermissionToSelf.DisableSounds = otherPreferred.DisableSounds;
|
||||
otherPermissionToSelf.DisableVFX = otherPreferred.DisableVFX;
|
||||
otherPermissionToSelf.IsPaused = otherPreferred.IsPaused;
|
||||
|
||||
DbContext.Update(otherPermissionToSelf);
|
||||
}
|
||||
|
||||
await Clients.User(UserUID).Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(),
|
||||
pair.ToUserData(), ownPermissionsToOther.ToUserPermissions(setSticky: ownPermissionsToOther.Sticky),
|
||||
otherPermissionToSelf.ToUserPermissions(setSticky: false))).ConfigureAwait(false);
|
||||
await Clients.User(pair.GroupUserUID).Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(),
|
||||
self.ToUserData(), otherPermissionToSelf.ToUserPermissions(setSticky: otherPermissionToSelf.Sticky),
|
||||
ownPermissionsToOther.ToUserPermissions(setSticky: false))).ConfigureAwait(false);
|
||||
|
||||
// if not paired prior and neither has the permissions set to paused, send online
|
||||
if ((!allUserPairs.ContainsKey(pair.GroupUserUID) || (allUserPairs.TryGetValue(pair.GroupUserUID, out var info) && !info.IsSynced))
|
||||
&& !otherPermissionToSelf.IsPaused && !ownPermissionsToOther.IsPaused)
|
||||
{
|
||||
var groupUserIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(pair.ToUserData(), groupUserIdent)).ConfigureAwait(false);
|
||||
await Clients.User(pair.GroupUserUID).Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupLeave(GroupDto dto)
|
||||
{
|
||||
await UserLeaveGroup(dto, UserUID).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<int> GroupPrune(GroupDto dto, int days, bool execute)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, days, execute));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return -1;
|
||||
|
||||
var allGroupUsers = await DbContext.GroupPairs.Include(p => p.GroupUser).Include(p => p.Group)
|
||||
.Where(g => g.GroupGID == dto.Group.GID)
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
var usersToPrune = allGroupUsers.Where(p => !p.IsPinned && !p.IsModerator
|
||||
&& p.GroupUserUID != UserUID
|
||||
&& p.Group.OwnerUID != p.GroupUserUID
|
||||
&& p.GroupUser.LastLoggedIn.AddDays(days) < DateTime.UtcNow);
|
||||
|
||||
if (!execute) return usersToPrune.Count();
|
||||
|
||||
DbContext.GroupPairs.RemoveRange(usersToPrune);
|
||||
|
||||
foreach (var pair in usersToPrune)
|
||||
{
|
||||
await Clients.Users(allGroupUsers.Where(p => !usersToPrune.Contains(p)).Select(g => g.GroupUserUID))
|
||||
.Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return usersToPrune.Count();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupRemoveUser(GroupPairDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return;
|
||||
|
||||
var (userExists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!userExists) return;
|
||||
|
||||
if (groupPair.IsModerator || string.Equals(group.OwnerUID, dto.User.UID, StringComparison.Ordinal)) return;
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
DbContext.GroupPairs.Remove(groupPair);
|
||||
|
||||
var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList();
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairLeft(dto).ConfigureAwait(false);
|
||||
|
||||
var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false);
|
||||
if (userIdent == null)
|
||||
{
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await Clients.User(dto.User.UID).Client_GroupDelete(new GroupDto(dto.Group)).ConfigureAwait(false);
|
||||
|
||||
var userPairs = await GetAllPairInfo(dto.User.UID).ConfigureAwait(false);
|
||||
foreach (var groupUserPair in groupPairs)
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, userIdent, userPairs, dto.User.UID).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupSetUserInfo(GroupPairUserInfoDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (userExists, userPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!userExists) return;
|
||||
|
||||
var (userIsOwner, _) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
var (userIsModerator, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
|
||||
if (dto.GroupUserInfo.HasFlag(GroupPairUserInfo.IsPinned) && userIsModerator && !userPair.IsPinned)
|
||||
{
|
||||
userPair.IsPinned = true;
|
||||
}
|
||||
else if (userIsModerator && userPair.IsPinned)
|
||||
{
|
||||
userPair.IsPinned = false;
|
||||
}
|
||||
|
||||
if (dto.GroupUserInfo.HasFlag(GroupPairUserInfo.IsModerator) && userIsOwner && !userPair.IsModerator)
|
||||
{
|
||||
userPair.IsModerator = true;
|
||||
}
|
||||
else if (userIsOwner && userPair.IsModerator)
|
||||
{
|
||||
userPair.IsModerator = false;
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
|
||||
await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
var preferredPermissions = (await DbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == UserUID).ToListAsync().ConfigureAwait(false))
|
||||
.Where(u => groups.Exists(k => string.Equals(k.GroupGID, u.GroupGID, StringComparison.Ordinal)))
|
||||
.ToDictionary(u => groups.First(f => string.Equals(f.GroupGID, u.GroupGID, StringComparison.Ordinal)), u => u);
|
||||
var groupInfos = await DbContext.GroupPairs.Where(u => groups.Select(g => g.GroupGID).Contains(u.GroupGID) && (u.IsPinned || u.IsModerator))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
|
||||
return preferredPermissions.Select(g => new GroupFullInfoDto(g.Key.Group.ToGroupData(), g.Key.Group.Owner.ToUserData(),
|
||||
g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(),
|
||||
groupInfos.Where(i => string.Equals(i.GroupGID, g.Key.GroupGID, StringComparison.Ordinal))
|
||||
.ToDictionary(i => i.GroupUserUID, i => i.ToEnum(), StringComparer.Ordinal))).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupUnbanUser(GroupPairDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!userHasRights) return;
|
||||
|
||||
var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (banEntry == null) return;
|
||||
|
||||
DbContext.Remove(banEntry);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSyncServer.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public partial class LightlessHub
|
||||
{
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissions)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(defaultPermissions));
|
||||
|
||||
var permissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false);
|
||||
|
||||
permissions.DisableGroupAnimations = defaultPermissions.DisableGroupAnimations;
|
||||
permissions.DisableGroupSounds = defaultPermissions.DisableGroupSounds;
|
||||
permissions.DisableGroupVFX = defaultPermissions.DisableGroupVFX;
|
||||
permissions.DisableIndividualAnimations = defaultPermissions.DisableIndividualAnimations;
|
||||
permissions.DisableIndividualSounds = defaultPermissions.DisableIndividualSounds;
|
||||
permissions.DisableIndividualVFX = defaultPermissions.DisableIndividualVFX;
|
||||
permissions.IndividualIsSticky = defaultPermissions.IndividualIsSticky;
|
||||
|
||||
DbContext.Update(permissions);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Caller.Client_UserUpdateDefaultPermissions(defaultPermissions).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task SetBulkPermissions(BulkPermissionsDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(
|
||||
"Individual", string.Join(';', dto.AffectedUsers.Select(g => g.Key + ":" + g.Value)),
|
||||
"Group", string.Join(';', dto.AffectedGroups.Select(g => g.Key + ":" + g.Value))));
|
||||
|
||||
// remove self
|
||||
dto.AffectedUsers.Remove(UserUID, out _);
|
||||
if (!dto.AffectedUsers.Any() && !dto.AffectedGroups.Any()) return;
|
||||
|
||||
// get all current pairs in any form
|
||||
var allUsers = await GetAllPairInfo(UserUID).ConfigureAwait(false);
|
||||
var ownDefaultPerms = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false);
|
||||
|
||||
foreach (var user in dto.AffectedUsers)
|
||||
{
|
||||
bool setSticky = false;
|
||||
var newPerm = user.Value;
|
||||
if (!allUsers.TryGetValue(user.Key, out var pairData)) continue;
|
||||
if (!pairData.OwnPermissions.Sticky && !newPerm.IsSticky())
|
||||
{
|
||||
setSticky = ownDefaultPerms.IndividualIsSticky;
|
||||
}
|
||||
|
||||
var pauseChange = pairData.OwnPermissions.IsPaused != newPerm.IsPaused();
|
||||
var prevPermissions = await DbContext.Permissions.SingleAsync(u => u.UserUID == UserUID && u.OtherUserUID == user.Key).ConfigureAwait(false);
|
||||
|
||||
prevPermissions.IsPaused = newPerm.IsPaused();
|
||||
prevPermissions.DisableAnimations = newPerm.IsDisableAnimations();
|
||||
prevPermissions.DisableSounds = newPerm.IsDisableSounds();
|
||||
prevPermissions.DisableVFX = newPerm.IsDisableVFX();
|
||||
prevPermissions.Sticky = newPerm.IsSticky() || setSticky;
|
||||
DbContext.Update(prevPermissions);
|
||||
|
||||
// send updated data to pair
|
||||
var permCopy = newPerm;
|
||||
permCopy.SetSticky(newPerm.IsSticky() || setSticky);
|
||||
var permToOther = permCopy;
|
||||
permToOther.SetSticky(false);
|
||||
|
||||
await Clients.User(UserUID).Client_UserUpdateSelfPairPermissions(new(new(user.Key), permCopy)).ConfigureAwait(false);
|
||||
if (pairData.OtherPermissions == null) continue;
|
||||
|
||||
await Clients.User(user.Key).Client_UserUpdateOtherPairPermissions(new(new(UserUID), permToOther)).ConfigureAwait(false);
|
||||
|
||||
// check if pause change and send online or offline respectively
|
||||
if (pauseChange && !pairData.OtherPermissions.IsPaused)
|
||||
{
|
||||
var otherCharaIdent = await GetUserIdent(user.Key).ConfigureAwait(false);
|
||||
|
||||
if (UserCharaIdent == null || otherCharaIdent == null) continue;
|
||||
|
||||
if (newPerm.IsPaused())
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOffline(new(new(user.Key))).ConfigureAwait(false);
|
||||
await Clients.User(user.Key).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(new(user.Key), otherCharaIdent)).ConfigureAwait(false);
|
||||
await Clients.User(user.Key).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in dto.AffectedGroups)
|
||||
{
|
||||
var (inGroup, groupPair) = await TryValidateUserInGroup(group.Key).ConfigureAwait(false);
|
||||
if (!inGroup) continue;
|
||||
|
||||
var groupPreferredPermissions = await DbContext.GroupPairPreferredPermissions
|
||||
.SingleAsync(u => u.UserUID == UserUID && u.GroupGID == group.Key).ConfigureAwait(false);
|
||||
|
||||
var wasPaused = groupPreferredPermissions.IsPaused;
|
||||
groupPreferredPermissions.DisableSounds = group.Value.IsDisableSounds();
|
||||
groupPreferredPermissions.DisableAnimations = group.Value.IsDisableAnimations();
|
||||
groupPreferredPermissions.IsPaused = group.Value.IsPaused();
|
||||
groupPreferredPermissions.DisableVFX = group.Value.IsDisableVFX();
|
||||
|
||||
var nonStickyPairs = allUsers.Where(u => !u.Value.OwnPermissions.Sticky).ToList();
|
||||
var affectedGroupPairs = nonStickyPairs.Where(u => u.Value.GIDs.Contains(group.Key, StringComparer.Ordinal)).ToList();
|
||||
var groupUserUids = affectedGroupPairs.Select(g => g.Key).ToList();
|
||||
var affectedPerms = await DbContext.Permissions.Where(u => u.UserUID == UserUID
|
||||
&& groupUserUids.Any(c => c == u.OtherUserUID))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var perm in affectedPerms)
|
||||
{
|
||||
perm.DisableSounds = groupPreferredPermissions.DisableSounds;
|
||||
perm.DisableAnimations = groupPreferredPermissions.DisableAnimations;
|
||||
perm.IsPaused = groupPreferredPermissions.IsPaused;
|
||||
perm.DisableVFX = groupPreferredPermissions.DisableVFX;
|
||||
}
|
||||
|
||||
UserPermissions permissions = UserPermissions.NoneSet;
|
||||
permissions.SetPaused(groupPreferredPermissions.IsPaused);
|
||||
permissions.SetDisableAnimations(groupPreferredPermissions.DisableAnimations);
|
||||
permissions.SetDisableSounds(groupPreferredPermissions.DisableSounds);
|
||||
permissions.SetDisableVFX(groupPreferredPermissions.DisableVFX);
|
||||
|
||||
await Clients.Users(affectedGroupPairs
|
||||
.Select(k => k.Key))
|
||||
.Client_UserUpdateOtherPairPermissions(new(new(UserUID), permissions)).ConfigureAwait(false);
|
||||
|
||||
await Clients.User(UserUID).Client_GroupChangeUserPairPermissions(new GroupPairUserPermissionDto(new(group.Key), new(UserUID), group.Value)).ConfigureAwait(false);
|
||||
foreach (var item in affectedGroupPairs.Select(k => k.Key))
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserUpdateSelfPairPermissions(new(new(item), permissions)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (wasPaused == groupPreferredPermissions.IsPaused) continue;
|
||||
|
||||
foreach (var groupUserPair in affectedGroupPairs)
|
||||
{
|
||||
var groupUserIdent = await GetUserIdent(groupUserPair.Key).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent) && !groupUserPair.Value.OtherPermissions.IsPaused)
|
||||
{
|
||||
// if we changed to paused and other was not paused before, we send offline
|
||||
if (groupPreferredPermissions.IsPaused)
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOffline(new(new(groupUserPair.Key, groupUserPair.Value.Alias))).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.Key).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false);
|
||||
}
|
||||
// if we changed to unpaused and other was not paused either we send online
|
||||
else
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(new(groupUserPair.Key, groupUserPair.Value.Alias), groupUserIdent)).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.Key).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
437
LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs
Normal file
437
LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs
Normal file
@@ -0,0 +1,437 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSyncServer.Utils;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public partial class LightlessHub
|
||||
{
|
||||
private static readonly string[] AllowedExtensionsForGamePaths = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk" };
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserAddPair(UserDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
// don't allow adding nothing
|
||||
var uid = dto.User.UID.Trim();
|
||||
if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return;
|
||||
|
||||
// grab other user, check if it exists and if a pair already exists
|
||||
var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false);
|
||||
if (otherUser == null)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(otherUser.UID, UserUID, StringComparison.Ordinal))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"My god you can't pair with yourself why would you do that please stop").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var existingEntry =
|
||||
await DbContext.ClientPairs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.User.UID == UserUID && p.OtherUserUID == otherUser.UID).ConfigureAwait(false);
|
||||
|
||||
if (existingEntry != null)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, already paired").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// grab self create new client pair and save
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
ClientPair wl = new ClientPair()
|
||||
{
|
||||
OtherUser = otherUser,
|
||||
User = user,
|
||||
};
|
||||
await DbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false);
|
||||
|
||||
var existingData = await GetPairInfo(UserUID, otherUser.UID).ConfigureAwait(false);
|
||||
|
||||
var permissions = existingData?.OwnPermissions;
|
||||
if (permissions == null || !permissions.Sticky)
|
||||
{
|
||||
var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID).ConfigureAwait(false);
|
||||
|
||||
permissions = new UserPermissionSet()
|
||||
{
|
||||
User = user,
|
||||
OtherUser = otherUser,
|
||||
DisableAnimations = ownDefaultPermissions.DisableIndividualAnimations,
|
||||
DisableSounds = ownDefaultPermissions.DisableIndividualSounds,
|
||||
DisableVFX = ownDefaultPermissions.DisableIndividualVFX,
|
||||
IsPaused = false,
|
||||
Sticky = true
|
||||
};
|
||||
|
||||
var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID).ConfigureAwait(false);
|
||||
if (existingDbPerms == null)
|
||||
{
|
||||
await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingDbPerms.DisableAnimations = permissions.DisableAnimations;
|
||||
existingDbPerms.DisableSounds = permissions.DisableSounds;
|
||||
existingDbPerms.DisableVFX = permissions.DisableVFX;
|
||||
existingDbPerms.IsPaused = false;
|
||||
existingDbPerms.Sticky = true;
|
||||
|
||||
DbContext.Permissions.Update(existingDbPerms);
|
||||
}
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
// get the opposite entry of the client pair
|
||||
var otherEntry = OppositeEntry(otherUser.UID);
|
||||
var otherIdent = await GetUserIdent(otherUser.UID).ConfigureAwait(false);
|
||||
|
||||
var otherPermissions = existingData?.OtherPermissions ?? null;
|
||||
|
||||
var ownPerm = permissions.ToUserPermissions(setSticky: true);
|
||||
var otherPerm = otherPermissions.ToUserPermissions();
|
||||
|
||||
var userPairResponse = new UserPairDto(otherUser.ToUserData(),
|
||||
otherEntry == null ? IndividualPairStatus.OneSided : IndividualPairStatus.Bidirectional,
|
||||
ownPerm, otherPerm);
|
||||
|
||||
await Clients.User(user.UID).Client_UserAddClientPair(userPairResponse).ConfigureAwait(false);
|
||||
|
||||
// check if other user is online
|
||||
if (otherIdent == null || otherEntry == null) return;
|
||||
|
||||
// send push with update to other user if other user is online
|
||||
await Clients.User(otherUser.UID)
|
||||
.Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(user.ToUserData(),
|
||||
permissions.ToUserPermissions())).ConfigureAwait(false);
|
||||
|
||||
await Clients.User(otherUser.UID)
|
||||
.Client_UpdateUserIndividualPairStatusDto(new(user.ToUserData(), IndividualPairStatus.Bidirectional))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!ownPerm.IsPaused() && !otherPerm.IsPaused())
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(otherUser.ToUserData(), otherIdent)).ConfigureAwait(false);
|
||||
await Clients.User(otherUser.UID).Client_UserSendOnline(new(user.ToUserData(), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserDelete()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
var secondaryUsers = await DbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var user in secondaryUsers)
|
||||
{
|
||||
await DeleteUser(user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await DeleteUser(userEntry).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs(CensusDataDto? censusData)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||
|
||||
await SendOnlineToAllPairedUsers().ConfigureAwait(false);
|
||||
|
||||
_lightlessCensus.PublishStatistics(UserUID, censusData);
|
||||
|
||||
return pairs.Select(p => new OnlineUserIdentDto(new UserData(p.Key), p.Value)).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<UserFullPairDto>> UserGetPairedClients()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var pairs = await GetAllPairInfo(UserUID).ConfigureAwait(false);
|
||||
return pairs.Select(p =>
|
||||
{
|
||||
return new UserFullPairDto(new UserData(p.Key, p.Value.Alias),
|
||||
p.Value.ToIndividualPairStatus(),
|
||||
p.Value.GIDs.Where(g => !string.Equals(g, Constants.IndividualKeyword, StringComparison.OrdinalIgnoreCase)).ToList(),
|
||||
p.Value.OwnPermissions.ToUserPermissions(setSticky: true),
|
||||
p.Value.OtherPermissions.ToUserPermissions());
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<UserProfileDto> UserGetProfile(UserDto user)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(user));
|
||||
|
||||
var allUserPairs = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
|
||||
if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal))
|
||||
{
|
||||
return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile.");
|
||||
}
|
||||
|
||||
var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID).ConfigureAwait(false);
|
||||
if (data == null) return new UserProfileDto(user.User, false, null, null, null);
|
||||
|
||||
if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation");
|
||||
if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled");
|
||||
|
||||
return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserPushData(UserCharaDataMessageDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto.CharaData.FileReplacements.Count));
|
||||
|
||||
// check for honorific containing . and /
|
||||
try
|
||||
{
|
||||
var honorificJson = Encoding.Default.GetString(Convert.FromBase64String(dto.CharaData.HonorificData));
|
||||
var deserialized = JsonSerializer.Deserialize<JsonElement>(honorificJson);
|
||||
if (deserialized.TryGetProperty("Title", out var honorificTitle))
|
||||
{
|
||||
var title = honorificTitle.GetString().Normalize(NormalizationForm.FormKD);
|
||||
if (UrlRegex().IsMatch(title))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your data was not pushed: The usage of URLs the Honorific titles is prohibited. Remove them to be able to continue to push data.").ConfigureAwait(false);
|
||||
throw new HubException("Invalid data provided, Honorific title invalid: " + title);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HubException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
|
||||
bool hadInvalidData = false;
|
||||
List<string> invalidGamePaths = new();
|
||||
List<string> invalidFileSwapPaths = new();
|
||||
foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value))
|
||||
{
|
||||
var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList();
|
||||
invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||
replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).ToArray();
|
||||
bool validGamePaths = replacement.GamePaths.Any();
|
||||
bool validHash = string.IsNullOrEmpty(replacement.Hash) || HashRegex().IsMatch(replacement.Hash);
|
||||
bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || GamePathRegex().IsMatch(replacement.FileSwapPath);
|
||||
if (!validGamePaths || !validHash || !validFileSwapPath)
|
||||
{
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args("Invalid Data", "GamePaths", validGamePaths, string.Join(",", invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath));
|
||||
hadInvalidData = true;
|
||||
if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath);
|
||||
if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths);
|
||||
if (!validHash) invalidFileSwapPaths.Add(replacement.Hash);
|
||||
}
|
||||
}
|
||||
|
||||
if (hadInvalidData)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "One or more of your supplied mods were rejected from the server. Consult /xllog for more information.").ConfigureAwait(false);
|
||||
throw new HubException("Invalid data provided, contact the appropriate mod creator to resolve those issues"
|
||||
+ Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, invalidGamePaths.Select(p => "Invalid Game Path: " + p))
|
||||
+ Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, invalidFileSwapPaths.Select(p => "Invalid FileSwap Path: " + p)));
|
||||
}
|
||||
|
||||
var recipientUids = dto.Recipients.Select(r => r.UID).ToList();
|
||||
bool allCached = await _onlineSyncedPairCacheService.AreAllPlayersCached(UserUID,
|
||||
recipientUids, Context.ConnectionAborted).ConfigureAwait(false);
|
||||
|
||||
if (!allCached)
|
||||
{
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
|
||||
recipientUids = allPairedUsers.Where(f => recipientUids.Contains(f, StringComparer.Ordinal)).ToList();
|
||||
|
||||
await _onlineSyncedPairCacheService.CachePlayers(UserUID, allPairedUsers, Context.ConnectionAborted).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(recipientUids.Count));
|
||||
|
||||
await Clients.Users(recipientUids).Client_UserReceiveCharacterData(new OnlineUserCharaDataDto(new UserData(UserUID), dto.CharaData)).ConfigureAwait(false);
|
||||
|
||||
_lightlessCensus.PublishStatistics(UserUID, dto.CensusDataDto);
|
||||
|
||||
_lightlessMetrics.IncCounter(MetricsAPI.CounterUserPushData);
|
||||
_lightlessMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipientUids.Count);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserRemovePair(UserDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) return;
|
||||
|
||||
// check if client pair even exists
|
||||
ClientPair callerPair =
|
||||
await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (callerPair == null) return;
|
||||
|
||||
var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false);
|
||||
|
||||
// delete from database, send update info to users pair list
|
||||
DbContext.ClientPairs.Remove(callerPair);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
await Clients.User(UserUID).Client_UserRemoveClientPair(dto).ConfigureAwait(false);
|
||||
|
||||
// check if opposite entry exists
|
||||
if (!pairData.IndividuallyPaired) return;
|
||||
|
||||
// check if other user is online, if no then there is no need to do anything further
|
||||
var otherIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false);
|
||||
if (otherIdent == null) return;
|
||||
|
||||
// if the other user had paused the user the state will be offline for either, do nothing
|
||||
bool callerHadPaused = pairData.OwnPermissions?.IsPaused ?? false;
|
||||
|
||||
// send updated individual pair status
|
||||
await Clients.User(dto.User.UID)
|
||||
.Client_UpdateUserIndividualPairStatusDto(new(new(UserUID), IndividualPairStatus.OneSided))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
UserPermissionSet? otherPermissions = pairData.OtherPermissions;
|
||||
bool otherHadPaused = otherPermissions?.IsPaused ?? true;
|
||||
|
||||
// if the either had paused, do nothing
|
||||
if (callerHadPaused && otherHadPaused) return;
|
||||
|
||||
var currentPairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false);
|
||||
|
||||
// if neither user had paused each other and either is not in an unpaused group with each other, change state to offline
|
||||
if (!currentPairData?.IsSynced ?? true)
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false);
|
||||
await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserSetProfile(UserProfileDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself");
|
||||
|
||||
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false);
|
||||
|
||||
if (existingData?.FlaggedForReport ?? false)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingData?.ProfileDisabled ?? false)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
|
||||
{
|
||||
byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64);
|
||||
using MemoryStream ms = new(imageData);
|
||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
using var image = Image.Load<Rgba32>(imageData);
|
||||
|
||||
if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingData != null)
|
||||
{
|
||||
if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
existingData.Base64ProfileImage = null;
|
||||
}
|
||||
else if (dto.ProfilePictureBase64 != null)
|
||||
{
|
||||
existingData.Base64ProfileImage = dto.ProfilePictureBase64;
|
||||
}
|
||||
|
||||
if (dto.IsNSFW != null)
|
||||
{
|
||||
existingData.IsNSFW = dto.IsNSFW.Value;
|
||||
}
|
||||
|
||||
if (dto.Description != null)
|
||||
{
|
||||
existingData.UserDescription = dto.Description;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UserProfileData userProfileData = new()
|
||||
{
|
||||
UserUID = dto.User.UID,
|
||||
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
|
||||
UserDescription = dto.Description ?? null,
|
||||
IsNSFW = dto.IsNSFW ?? false
|
||||
};
|
||||
|
||||
await DbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(pairs.Select(p => p.Key)).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false);
|
||||
await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)]
|
||||
private static partial Regex GamePathRegex();
|
||||
|
||||
[GeneratedRegex(@"^[A-Z0-9]{40}$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)]
|
||||
private static partial Regex HashRegex();
|
||||
|
||||
[GeneratedRegex("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}[\\.,][a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$")]
|
||||
private static partial Regex UrlRegex();
|
||||
|
||||
private ClientPair OppositeEntry(string otherUID) =>
|
||||
DbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == UserUID);
|
||||
}
|
||||
212
LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs
Normal file
212
LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSyncServer.Services;
|
||||
using LightlessSyncServer.Utils;
|
||||
using LightlessSyncShared;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis.Extensions.Core.Abstractions;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, string> _userConnections = new(StringComparer.Ordinal);
|
||||
private readonly LightlessMetrics _lightlessMetrics;
|
||||
private readonly SystemInfoService _systemInfoService;
|
||||
private readonly IHttpContextAccessor _contextAccessor;
|
||||
private readonly LightlessHubLogger _logger;
|
||||
private readonly string _shardName;
|
||||
private readonly int _maxExistingGroupsByUser;
|
||||
private readonly int _maxJoinedGroupsByUser;
|
||||
private readonly int _maxGroupUserCount;
|
||||
private readonly IRedisDatabase _redis;
|
||||
private readonly OnlineSyncedPairCacheService _onlineSyncedPairCacheService;
|
||||
private readonly LightlessCensus _lightlessCensus;
|
||||
private readonly GPoseLobbyDistributionService _gPoseLobbyDistributionService;
|
||||
private readonly Uri _fileServerAddress;
|
||||
private readonly Version _expectedClientVersion;
|
||||
private readonly Lazy<LightlessDbContext> _dbContextLazy;
|
||||
private LightlessDbContext DbContext => _dbContextLazy.Value;
|
||||
private readonly int _maxCharaDataByUser;
|
||||
private readonly int _maxCharaDataByUserVanity;
|
||||
|
||||
public LightlessHub(LightlessMetrics lightlessMetrics,
|
||||
IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, ILogger<LightlessHub> logger, SystemInfoService systemInfoService,
|
||||
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
||||
IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus,
|
||||
GPoseLobbyDistributionService gPoseLobbyDistributionService)
|
||||
{
|
||||
_lightlessMetrics = lightlessMetrics;
|
||||
_systemInfoService = systemInfoService;
|
||||
_shardName = configuration.GetValue<string>(nameof(ServerConfiguration.ShardName));
|
||||
_maxExistingGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3);
|
||||
_maxJoinedGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxJoinedGroupsByUser), 6);
|
||||
_maxGroupUserCount = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 100);
|
||||
_fileServerAddress = configuration.GetValue<Uri>(nameof(ServerConfiguration.CdnFullUrl));
|
||||
_expectedClientVersion = configuration.GetValueOrDefault(nameof(ServerConfiguration.ExpectedClientVersion), new Version(0, 0, 0));
|
||||
_maxCharaDataByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUser), 10);
|
||||
_maxCharaDataByUserVanity = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUserVanity), 50);
|
||||
_contextAccessor = contextAccessor;
|
||||
_redis = redisDb;
|
||||
_onlineSyncedPairCacheService = onlineSyncedPairCacheService;
|
||||
_lightlessCensus = lightlessCensus;
|
||||
_gPoseLobbyDistributionService = gPoseLobbyDistributionService;
|
||||
_logger = new LightlessHubLogger(this, logger);
|
||||
_dbContextLazy = new Lazy<LightlessDbContext>(() => lightlessDbContextFactory.CreateDbContext());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_dbContextLazy.IsValueCreated) DbContext.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<ConnectionDto> GetConnectionDto()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
_lightlessMetrics.IncCounter(MetricsAPI.CounterInitializedConnections);
|
||||
|
||||
await Clients.Caller.Client_UpdateSystemInfo(_systemInfoService.SystemInfoDto).ConfigureAwait(false);
|
||||
|
||||
var dbUser = await DbContext.Users.SingleAsync(f => f.UID == UserUID).ConfigureAwait(false);
|
||||
dbUser.LastLoggedIn = DateTime.UtcNow;
|
||||
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Lightless Sync \"" + _shardName + "\", Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false);
|
||||
|
||||
var defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID).ConfigureAwait(false);
|
||||
if (defaultPermissions == null)
|
||||
{
|
||||
defaultPermissions = new UserDefaultPreferredPermission()
|
||||
{
|
||||
UserUID = UserUID,
|
||||
};
|
||||
|
||||
DbContext.UserDefaultPreferredPermissions.Add(defaultPermissions);
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias))
|
||||
{
|
||||
CurrentClientVersion = _expectedClientVersion,
|
||||
ServerVersion = ILightlessHub.ApiVersion,
|
||||
IsAdmin = dbUser.IsAdmin,
|
||||
IsModerator = dbUser.IsModerator,
|
||||
ServerInfo = new ServerInfo()
|
||||
{
|
||||
MaxGroupsCreatedByUser = _maxExistingGroupsByUser,
|
||||
ShardName = _shardName,
|
||||
MaxGroupsJoinedByUser = _maxJoinedGroupsByUser,
|
||||
MaxGroupUserCount = _maxGroupUserCount,
|
||||
FileServerAddress = _fileServerAddress,
|
||||
MaxCharaData = _maxCharaDataByUser,
|
||||
MaxCharaDataVanity = _maxCharaDataByUserVanity,
|
||||
},
|
||||
DefaultPreferredPermissions = new DefaultPermissionsDto()
|
||||
{
|
||||
DisableGroupAnimations = defaultPermissions.DisableGroupAnimations,
|
||||
DisableGroupSounds = defaultPermissions.DisableGroupSounds,
|
||||
DisableGroupVFX = defaultPermissions.DisableGroupVFX,
|
||||
DisableIndividualAnimations = defaultPermissions.DisableIndividualAnimations,
|
||||
DisableIndividualSounds = defaultPermissions.DisableIndividualSounds,
|
||||
DisableIndividualVFX = defaultPermissions.DisableIndividualVFX,
|
||||
IndividualIsSticky = defaultPermissions.IndividualIsSticky,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public async Task<bool> CheckClientHealth()
|
||||
{
|
||||
await UpdateUserOnRedis().ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
if (_userConnections.TryGetValue(UserUID, out var oldId))
|
||||
{
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), "UpdatingId", oldId, Context.ConnectionId));
|
||||
_userConnections[UserUID] = Context.ConnectionId;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent));
|
||||
await _onlineSyncedPairCacheService.InitPlayer(UserUID).ConfigureAwait(false);
|
||||
await UpdateUserOnRedis().ConfigureAwait(false);
|
||||
_userConnections[UserUID] = Context.ConnectionId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_userConnections.Remove(UserUID, out _);
|
||||
}
|
||||
}
|
||||
|
||||
await base.OnConnectedAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
if (_userConnections.TryGetValue(UserUID, out var connectionId)
|
||||
&& string.Equals(connectionId, Context.ConnectionId, StringComparison.Ordinal))
|
||||
{
|
||||
_lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
|
||||
|
||||
try
|
||||
{
|
||||
await GposeLobbyLeave().ConfigureAwait(false);
|
||||
|
||||
await _onlineSyncedPairCacheService.DisposePlayer(UserUID).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent));
|
||||
if (exception != null)
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, exception.Message, exception.StackTrace));
|
||||
|
||||
await RemoveUserFromRedis().ConfigureAwait(false);
|
||||
|
||||
_lightlessCensus.ClearStatistics(UserUID);
|
||||
|
||||
await SendOfflineToAllPairedUsers().ConfigureAwait(false);
|
||||
|
||||
DbContext.RemoveRange(DbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == UserUID));
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_userConnections.Remove(UserUID, out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), "ObsoleteId", UserUID, Context.ConnectionId));
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using AspNetCoreRateLimit;
|
||||
using LightlessSyncShared;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
public class SignalRLimitFilter : IHubFilter
|
||||
{
|
||||
private readonly IRateLimitProcessor _processor;
|
||||
private readonly IHttpContextAccessor accessor;
|
||||
private readonly ILogger<SignalRLimitFilter> logger;
|
||||
private static readonly SemaphoreSlim ConnectionLimiterSemaphore = new(20, 20);
|
||||
private static readonly SemaphoreSlim DisconnectLimiterSemaphore = new(20, 20);
|
||||
|
||||
public SignalRLimitFilter(
|
||||
IOptions<IpRateLimitOptions> options, IProcessingStrategy processing, IIpPolicyStore policyStore, IHttpContextAccessor accessor, ILogger<SignalRLimitFilter> logger)
|
||||
{
|
||||
_processor = new IpRateLimitProcessor(options?.Value, policyStore, processing);
|
||||
this.accessor = accessor;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask<object> InvokeMethodAsync(
|
||||
HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
|
||||
{
|
||||
var ip = accessor.GetIpAddress();
|
||||
var client = new ClientRequestIdentity
|
||||
{
|
||||
ClientIp = ip,
|
||||
Path = invocationContext.HubMethodName,
|
||||
HttpVerb = "ws",
|
||||
ClientId = invocationContext.Context.UserIdentifier,
|
||||
};
|
||||
foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false))
|
||||
{
|
||||
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
|
||||
if (counter.Count > rule.Limit)
|
||||
{
|
||||
var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? "Unknown";
|
||||
var retry = counter.Timestamp.RetryAfterFrom(rule);
|
||||
logger.LogWarning("Method rate limit triggered from {ip}/{authUserId}: {method}", ip, authUserId, invocationContext.HubMethodName);
|
||||
throw new HubException($"call limit {retry}");
|
||||
}
|
||||
}
|
||||
|
||||
return await next(invocationContext).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Optional method
|
||||
/* public async Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
|
||||
{
|
||||
await ConnectionLimiterSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var ip = accessor.GetIpAddress();
|
||||
var client = new ClientRequestIdentity
|
||||
{
|
||||
ClientIp = ip,
|
||||
Path = "Connect",
|
||||
HttpVerb = "ws",
|
||||
};
|
||||
foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false))
|
||||
{
|
||||
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
|
||||
if (counter.Count > rule.Limit)
|
||||
{
|
||||
var retry = counter.Timestamp.RetryAfterFrom(rule);
|
||||
logger.LogWarning("Connection rate limit triggered from {ip}", ip);
|
||||
ConnectionLimiterSemaphore.Release();
|
||||
throw new HubException($"Connection rate limit {retry}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await Task.Delay(25).ConfigureAwait(false);
|
||||
await next(context).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error on OnConnectedAsync");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ConnectionLimiterSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnDisconnectedAsync(
|
||||
HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next)
|
||||
{
|
||||
await DisconnectLimiterSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
if (exception != null)
|
||||
{
|
||||
logger.LogWarning(exception, "InitialException on OnDisconnectedAsync");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await next(context, exception).ConfigureAwait(false);
|
||||
await Task.Delay(25).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "ThrownException on OnDisconnectedAsync");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisconnectLimiterSemaphore.Release();
|
||||
}
|
||||
} */
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<UserSecretsId>aspnet-LightlessSyncServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="appsettings.Development.json" />
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.Development.json" />
|
||||
<None Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.8" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||
<ProjectReference Include="..\LightlessSyncShared\LightlessSyncShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
96
LightlessSyncServer/LightlessSyncServer/Program.cs
Normal file
96
LightlessSyncServer/LightlessSyncServer/Program.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
namespace LightlessSyncServer;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var hostBuilder = CreateHostBuilder(args);
|
||||
using var host = hostBuilder.Build();
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
var factory = services.GetRequiredService<IDbContextFactory<LightlessDbContext>>();
|
||||
using var context = factory.CreateDbContext();
|
||||
var options = services.GetRequiredService<IConfigurationService<ServerConfiguration>>();
|
||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
if (options.IsMain)
|
||||
{
|
||||
context.Database.SetCommandTimeout(TimeSpan.FromMinutes(10));
|
||||
context.Database.Migrate();
|
||||
context.Database.SetCommandTimeout(TimeSpan.FromSeconds(30));
|
||||
context.SaveChanges();
|
||||
|
||||
// clean up residuals
|
||||
var looseFiles = context.Files.Where(f => f.Uploaded == false);
|
||||
var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null);
|
||||
context.RemoveRange(unfinishedRegistrations);
|
||||
context.RemoveRange(looseFiles);
|
||||
context.SaveChanges();
|
||||
|
||||
logger.LogInformation(options.ToString());
|
||||
}
|
||||
var metrics = services.GetRequiredService<LightlessMetrics>();
|
||||
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, context.Users.AsNoTracking().Count());
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count());
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.Permissions.AsNoTracking().Where(p=>p.IsPaused).Count());
|
||||
|
||||
}
|
||||
|
||||
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
|
||||
{
|
||||
try
|
||||
{
|
||||
host.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args)
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddConsole();
|
||||
});
|
||||
var logger = loggerFactory.CreateLogger<Startup>();
|
||||
return Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureAppConfiguration((ctx, config) =>
|
||||
{
|
||||
var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH");
|
||||
if (!string.IsNullOrEmpty(appSettingsPath))
|
||||
{
|
||||
config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
||||
}
|
||||
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseContentRoot(AppContext.BaseDirectory);
|
||||
webBuilder.ConfigureLogging((ctx, builder) =>
|
||||
{
|
||||
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
|
||||
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
|
||||
});
|
||||
webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profiles": {
|
||||
"LightlessSyncServer": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": false,
|
||||
//"applicationUrl": "https://localhost:5001;http://localhost:5000;https://192.168.1.124:5001;http://192.168.1.124:5000",
|
||||
"applicationUrl": "http://localhost:5000;https://localhost:5001;https://darkarchon.internet-box.ch:5001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public class CharaDataCleanupService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<CharaDataCleanupService> _logger;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
|
||||
|
||||
public CharaDataCleanupService(ILogger<CharaDataCleanupService> logger, IDbContextFactory<LightlessDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await base.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Chara Data Cleanup Service started");
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("CharaData Cleanup Service started");
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
using (var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
var dateTime = DateTime.UtcNow;
|
||||
var expiredData = await db.CharaData.Where(c => c.ExpiryDate <= DateTime.UtcNow).ToListAsync(cancellationToken: ct).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Removing {count} expired Chara Data entries", expiredData.Count);
|
||||
|
||||
db.RemoveRange(expiredData);
|
||||
await db.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromHours(12), ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public class ClientPairPermissionsCleanupService(ILogger<ClientPairPermissionsCleanupService> _logger, IDbContextFactory<LightlessDbContext> _dbContextFactory,
|
||||
IConfigurationService<ServerConfiguration> _configurationService)
|
||||
: BackgroundService
|
||||
{
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Client Pair Permissions Cleanup Service started");
|
||||
await base.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task AllUsersPermissionsCleanup(CancellationToken ct)
|
||||
{
|
||||
const int MaxParallelism = 8;
|
||||
const int MaxProcessingPerChunk = 1000000;
|
||||
|
||||
long removedEntries = 0;
|
||||
long priorRemovedEntries = 0;
|
||||
ConcurrentDictionary<int, List<UserPermissionSet>> toRemovePermsParallel = [];
|
||||
ConcurrentDictionary<int, bool> completionDebugPrint = [];
|
||||
int parallelProcessed = 0;
|
||||
int userNo = 0;
|
||||
int lastUserNo = 0;
|
||||
|
||||
using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
|
||||
_logger.LogInformation("Building All Pairs");
|
||||
|
||||
_logger.LogInformation("Collecting Users");
|
||||
var users = (await db.Users.Select(k => k.UID).AsNoTracking().ToListAsync(ct).ConfigureAwait(false)).Order(StringComparer.Ordinal).ToList();
|
||||
|
||||
Stopwatch st = Stopwatch.StartNew();
|
||||
|
||||
while (userNo < users.Count)
|
||||
{
|
||||
using CancellationTokenSource loopCts = new();
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(loopCts.Token, ct);
|
||||
try
|
||||
{
|
||||
await Parallel.ForAsync(userNo, users.Count, new ParallelOptions()
|
||||
{
|
||||
MaxDegreeOfParallelism = MaxParallelism,
|
||||
CancellationToken = linkedCts.Token
|
||||
},
|
||||
async (i, token) =>
|
||||
{
|
||||
var userNoInc = Interlocked.Increment(ref userNo);
|
||||
using var db2 = await _dbContextFactory.CreateDbContextAsync(token).ConfigureAwait(false);
|
||||
|
||||
var user = users[i];
|
||||
var personalPairs = await GetAllPairsForUser(user, db2, ct).ConfigureAwait(false);
|
||||
|
||||
toRemovePermsParallel[i] = await UserPermissionCleanup(i, users.Count, user, db2, personalPairs).ConfigureAwait(false);
|
||||
var processedAdd = Interlocked.Add(ref parallelProcessed, toRemovePermsParallel[i].Count);
|
||||
|
||||
var completionPcnt = userNoInc / (double)users.Count;
|
||||
var completionInt = (int)(completionPcnt * 100);
|
||||
|
||||
if (completionInt > 0 && (!completionDebugPrint.TryGetValue(completionInt, out bool posted) || !posted))
|
||||
{
|
||||
completionDebugPrint[completionInt] = true;
|
||||
var elapsed = st.Elapsed;
|
||||
var estimatedTimeLeft = (elapsed / completionPcnt) - elapsed;
|
||||
_logger.LogInformation("Progress: {no}/{total} ({pct:P2}), removed so far: {removed}, planned next chunk: {planned}, estimated time left: {time}",
|
||||
userNoInc, users.Count, completionPcnt, removedEntries, processedAdd, estimatedTimeLeft);
|
||||
if (userNoInc / (double)users.Count - lastUserNo / (double)users.Count > 0.05)
|
||||
{
|
||||
// 5% processed without writing, might as well save at this point
|
||||
await loopCts.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedAdd > MaxProcessingPerChunk)
|
||||
await loopCts.CancelAsync().ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
|
||||
removedEntries += parallelProcessed;
|
||||
|
||||
try
|
||||
{
|
||||
parallelProcessed = 0;
|
||||
|
||||
_logger.LogInformation("Removing {newDeleted} entities and writing to database", removedEntries - priorRemovedEntries);
|
||||
db.Permissions.RemoveRange(toRemovePermsParallel.Values.SelectMany(v => v).ToList());
|
||||
await db.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Removed {newDeleted} entities, settling...", removedEntries - priorRemovedEntries);
|
||||
priorRemovedEntries = removedEntries;
|
||||
lastUserNo = userNo;
|
||||
}
|
||||
catch (DBConcurrencyException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Concurrency Exception during User Permissions Cleanup, restarting at {last}", lastUserNo);
|
||||
userNo = lastUserNo;
|
||||
removedEntries = priorRemovedEntries;
|
||||
continue;
|
||||
}
|
||||
finally
|
||||
{
|
||||
toRemovePermsParallel.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
st.Stop();
|
||||
_logger.LogInformation("User Permissions Cleanup Finished, removed {total} stale permissions in {time}", removedEntries, st.Elapsed);
|
||||
}
|
||||
|
||||
private async Task<List<UserPermissionSet>> UserPermissionCleanup(int userNr, int totalUsers, string uid, LightlessDbContext dbContext, List<string> pairs)
|
||||
{
|
||||
var perms = dbContext.Permissions.Where(p => p.UserUID == uid && !p.Sticky && !pairs.Contains(p.OtherUserUID));
|
||||
|
||||
var permsToRemoveCount = await perms.CountAsync().ConfigureAwait(false);
|
||||
if (permsToRemoveCount == 0)
|
||||
return [];
|
||||
|
||||
_logger.LogInformation("[{current}/{totalCount}] User {user}: Planning to remove {removed} permissions", userNr, totalUsers, uid, permsToRemoveCount);
|
||||
|
||||
return await perms.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetAllPairsForUser(string uid, LightlessDbContext dbContext, CancellationToken ct)
|
||||
{
|
||||
var entries = await dbContext.ClientPairs.AsNoTracking().Where(k => k.UserUID == uid).Select(k => k.OtherUserUID)
|
||||
.Concat(
|
||||
dbContext.GroupPairs.Where(k => k.GroupUserUID == uid).AsNoTracking()
|
||||
.Join(dbContext.GroupPairs.AsNoTracking(),
|
||||
a => a.GroupGID,
|
||||
b => b.GroupGID,
|
||||
(a, b) => b.GroupUserUID)
|
||||
.Where(a => a != uid))
|
||||
.ToListAsync(ct).ConfigureAwait(false);
|
||||
|
||||
return entries.Distinct(StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
if (!_configurationService.GetValueOrDefault(nameof(ServerConfiguration.RunPermissionCleanupOnStartup), defaultValue: true))
|
||||
{
|
||||
await WaitUntilNextCleanup(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting Permissions Cleanup");
|
||||
await AllUsersPermissionsCleanup(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Concurrency Exception during User Permissions Cleanup");
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled Exception during User Permissions Cleanup");
|
||||
}
|
||||
|
||||
await WaitUntilNextCleanup(ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitUntilNextCleanup(CancellationToken token)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nextRun = new DateTime(now.Year, now.Month, now.Day, 12, 0, 0, DateTimeKind.Utc);
|
||||
if (now > nextRun) nextRun = nextRun.AddDays(1);
|
||||
|
||||
var nextRunSpan = nextRun - now;
|
||||
_logger.LogInformation("Permissions Cleanup next run in {span}", nextRunSpan);
|
||||
|
||||
await Task.Delay(nextRunSpan, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSyncServer.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using StackExchange.Redis.Extensions.Core.Abstractions;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public sealed class GPoseLobbyDistributionService : IHostedService, IDisposable
|
||||
{
|
||||
private CancellationTokenSource _runtimeCts = new();
|
||||
private readonly Dictionary<string, Dictionary<string, WorldData>> _lobbyWorldData = [];
|
||||
private readonly Dictionary<string, Dictionary<string, PoseData>> _lobbyPoseData = [];
|
||||
private readonly SemaphoreSlim _lobbyPoseDataModificationSemaphore = new(1, 1);
|
||||
private readonly SemaphoreSlim _lobbyWorldDataModificationSemaphore = new(1, 1);
|
||||
|
||||
public GPoseLobbyDistributionService(ILogger<GPoseLobbyDistributionService> logger, IRedisDatabase redisDb,
|
||||
IHubContext<LightlessHub, ILightlessHub> hubContext)
|
||||
{
|
||||
_logger = logger;
|
||||
_redisDb = redisDb;
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private readonly ILogger<GPoseLobbyDistributionService> _logger;
|
||||
private readonly IRedisDatabase _redisDb;
|
||||
private readonly IHubContext<LightlessHub, ILightlessHub> _hubContext;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeCts.Cancel();
|
||||
_runtimeCts.Dispose();
|
||||
_lobbyPoseDataModificationSemaphore.Dispose();
|
||||
_lobbyWorldDataModificationSemaphore.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async Task PushWorldData(string lobby, string user, WorldData worldData)
|
||||
{
|
||||
await _lobbyWorldDataModificationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!_lobbyWorldData.TryGetValue(lobby, out var worldDataDict))
|
||||
{
|
||||
_lobbyWorldData[lobby] = worldDataDict = new(StringComparer.Ordinal);
|
||||
}
|
||||
worldDataDict[user] = worldData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pushing World Data for Lobby {lobby} by User {user}", lobby, user);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyWorldDataModificationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushPoseData(string lobby, string user, PoseData poseData)
|
||||
{
|
||||
await _lobbyPoseDataModificationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!_lobbyPoseData.TryGetValue(lobby, out var poseDataDict))
|
||||
{
|
||||
_lobbyPoseData[lobby] = poseDataDict = new(StringComparer.Ordinal);
|
||||
}
|
||||
poseDataDict[user] = poseData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pushing World Data for Lobby {lobby} by User {user}", lobby, user);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyPoseDataModificationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = WorldDataDistribution(_runtimeCts.Token);
|
||||
_ = PoseDataDistribution(_runtimeCts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task WorldDataDistribution(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DistributeWorldData(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during World Data Distribution");
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PoseDataDistribution(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DistributePoseData(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pose Data Distribution");
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DistributeWorldData(CancellationToken token)
|
||||
{
|
||||
await _lobbyWorldDataModificationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
Dictionary<string, Dictionary<string, WorldData>> clone = [];
|
||||
try
|
||||
{
|
||||
clone = _lobbyWorldData.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
|
||||
_lobbyWorldData.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Distributing World Data Clone generation");
|
||||
_lobbyWorldData.Clear();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyWorldDataModificationSemaphore.Release();
|
||||
}
|
||||
|
||||
foreach (var lobbyId in clone)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (!lobbyId.Value.Values.Any())
|
||||
continue;
|
||||
|
||||
var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false);
|
||||
if (gposeLobbyUsers == null)
|
||||
continue;
|
||||
|
||||
foreach (var data in lobbyId.Value)
|
||||
{
|
||||
await _hubContext.Clients.Users(gposeLobbyUsers.Where(k => !string.Equals(k, data.Key, StringComparison.Ordinal)))
|
||||
.Client_GposeLobbyPushWorldData(new(data.Key), data.Value).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during World Data Distribution for Lobby {lobby}", lobbyId.Key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DistributePoseData(CancellationToken token)
|
||||
{
|
||||
await _lobbyPoseDataModificationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
Dictionary<string, Dictionary<string, PoseData>> clone = [];
|
||||
try
|
||||
{
|
||||
clone = _lobbyPoseData.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
|
||||
_lobbyPoseData.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Distributing Pose Data Clone generation");
|
||||
_lobbyPoseData.Clear();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyPoseDataModificationSemaphore.Release();
|
||||
}
|
||||
|
||||
foreach (var lobbyId in clone)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (!lobbyId.Value.Values.Any())
|
||||
continue;
|
||||
|
||||
var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false);
|
||||
if (gposeLobbyUsers == null)
|
||||
continue;
|
||||
|
||||
foreach (var data in lobbyId.Value)
|
||||
{
|
||||
await _hubContext.Clients.Users(gposeLobbyUsers.Where(k => !string.Equals(k, data.Key, StringComparison.Ordinal)))
|
||||
.Client_GposeLobbyPushPoseData(new(data.Key), data.Value).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pose Data Distribution for Lobby {lobby}", lobbyId.Key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_runtimeCts.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
182
LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs
Normal file
182
LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using LightlessSync.API.Dto.User;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
using Prometheus;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public class LightlessCensus : IHostedService
|
||||
{
|
||||
private record CensusEntry(ushort WorldId, short Race, short Subrace, short Gender)
|
||||
{
|
||||
public static CensusEntry FromDto(CensusDataDto dto)
|
||||
{
|
||||
return new CensusEntry(dto.WorldId, dto.RaceId, dto.TribeId, dto.Gender);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, CensusEntry> _censusEntries = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<short, string> _dcs = new();
|
||||
private readonly Dictionary<short, string> _gender = new();
|
||||
private readonly ILogger<LightlessCensus> _logger;
|
||||
private readonly Dictionary<short, string> _races = new();
|
||||
private readonly Dictionary<short, string> _tribes = new();
|
||||
private readonly Dictionary<ushort, (string, short)> _worlds = new();
|
||||
private Gauge? _gauge;
|
||||
|
||||
public LightlessCensus(ILogger<LightlessCensus> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private bool Initialized => _gauge != null;
|
||||
|
||||
public void ClearStatistics(string uid)
|
||||
{
|
||||
if (!Initialized) return;
|
||||
|
||||
if (_censusEntries.Remove(uid, out var censusEntry))
|
||||
{
|
||||
ModifyGauge(censusEntry, increase: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void PublishStatistics(string uid, CensusDataDto? censusDataDto)
|
||||
{
|
||||
if (!Initialized || censusDataDto == null) return;
|
||||
|
||||
var newEntry = CensusEntry.FromDto(censusDataDto);
|
||||
|
||||
if (_censusEntries.TryGetValue(uid, out var entry))
|
||||
{
|
||||
if (entry != newEntry)
|
||||
{
|
||||
ModifyGauge(entry, increase: false);
|
||||
ModifyGauge(newEntry, increase: true);
|
||||
_censusEntries[uid] = newEntry;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_censusEntries[uid] = newEntry;
|
||||
ModifyGauge(newEntry, increase: true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Loading XIVAPI data");
|
||||
|
||||
using HttpClient client = new HttpClient();
|
||||
|
||||
Dictionary<ushort, short> worldDcs = new();
|
||||
|
||||
var dcs = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/WorldDCGroupType.csv", cancellationToken).ConfigureAwait(false);
|
||||
// dc: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/WorldDCGroupType.csv
|
||||
// id, name, region
|
||||
|
||||
using var dcsReader = new StringReader(dcs);
|
||||
using var dcsParser = new TextFieldParser(dcsReader);
|
||||
dcsParser.Delimiters = [","];
|
||||
// read 3 lines and discard
|
||||
dcsParser.ReadLine(); dcsParser.ReadLine(); dcsParser.ReadLine();
|
||||
|
||||
while (!dcsParser.EndOfData)
|
||||
{
|
||||
var fields = dcsParser.ReadFields();
|
||||
var id = short.Parse(fields[0], CultureInfo.InvariantCulture);
|
||||
var name = fields[1];
|
||||
if (string.IsNullOrEmpty(name) || id == 0) continue;
|
||||
_logger.LogInformation("DC: ID: {id}, Name: {name}", id, name);
|
||||
_dcs[id] = name;
|
||||
}
|
||||
|
||||
var worlds = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/World.csv", cancellationToken).ConfigureAwait(false);
|
||||
// world: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/World.csv
|
||||
// id, internalname, name, region, usertype, datacenter, ispublic
|
||||
|
||||
using var worldsReader = new StringReader(worlds);
|
||||
using var worldsParser = new TextFieldParser(worldsReader);
|
||||
worldsParser.Delimiters = [","];
|
||||
// read 3 lines and discard
|
||||
worldsParser.ReadLine(); worldsParser.ReadLine(); worldsParser.ReadLine();
|
||||
|
||||
while (!worldsParser.EndOfData)
|
||||
{
|
||||
var fields = worldsParser.ReadFields();
|
||||
var id = ushort.Parse(fields[0], CultureInfo.InvariantCulture);
|
||||
var name = fields[1];
|
||||
var dc = short.Parse(fields[5], CultureInfo.InvariantCulture);
|
||||
var isPublic = bool.Parse(fields[6]);
|
||||
if (!_dcs.ContainsKey(dc) || !isPublic) continue;
|
||||
_worlds[id] = (name, dc);
|
||||
_logger.LogInformation("World: ID: {id}, Name: {name}, DC: {dc}", id, name, dc);
|
||||
}
|
||||
|
||||
var races = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Race.csv", cancellationToken).ConfigureAwait(false);
|
||||
// race: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Race.csv
|
||||
// id, masc name, fem name, other crap I don't care about
|
||||
|
||||
using var raceReader = new StringReader(races);
|
||||
using var raceParser = new TextFieldParser(raceReader);
|
||||
raceParser.Delimiters = [","];
|
||||
// read 3 lines and discard
|
||||
raceParser.ReadLine(); raceParser.ReadLine(); raceParser.ReadLine();
|
||||
|
||||
while (!raceParser.EndOfData)
|
||||
{
|
||||
var fields = raceParser.ReadFields();
|
||||
var id = short.Parse(fields[0], CultureInfo.InvariantCulture);
|
||||
var name = fields[1];
|
||||
if (string.IsNullOrEmpty(name) || id == 0) continue;
|
||||
_races[id] = name;
|
||||
_logger.LogInformation("Race: ID: {id}, Name: {name}", id, name);
|
||||
}
|
||||
|
||||
var tribe = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Tribe.csv", cancellationToken).ConfigureAwait(false);
|
||||
// tribe: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Tribe.csv
|
||||
// id masc name, fem name, other crap I don't care about
|
||||
|
||||
using var tribeReader = new StringReader(tribe);
|
||||
using var tribeParser = new TextFieldParser(tribeReader);
|
||||
tribeParser.Delimiters = [","];
|
||||
// read 3 lines and discard
|
||||
tribeParser.ReadLine(); tribeParser.ReadLine(); tribeParser.ReadLine();
|
||||
|
||||
while (!tribeParser.EndOfData)
|
||||
{
|
||||
var fields = tribeParser.ReadFields();
|
||||
var id = short.Parse(fields[0], CultureInfo.InvariantCulture);
|
||||
var name = fields[1];
|
||||
if (string.IsNullOrEmpty(name) || id == 0) continue;
|
||||
_tribes[id] = name;
|
||||
_logger.LogInformation("Tribe: ID: {id}, Name: {name}", id, name);
|
||||
}
|
||||
|
||||
_gender[0] = "Male";
|
||||
_gender[1] = "Female";
|
||||
|
||||
_gauge = Metrics.CreateGauge("lightless_census", "lightless informational census data", new[] { "dc", "world", "gender", "race", "subrace" });
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ModifyGauge(CensusEntry censusEntry, bool increase)
|
||||
{
|
||||
var subraceSuccess = _tribes.TryGetValue(censusEntry.Subrace, out var subrace);
|
||||
var raceSuccess = _races.TryGetValue(censusEntry.Race, out var race);
|
||||
var worldSuccess = _worlds.TryGetValue(censusEntry.WorldId, out var world);
|
||||
var genderSuccess = _gender.TryGetValue(censusEntry.Gender, out var gender);
|
||||
if (subraceSuccess && raceSuccess && worldSuccess && genderSuccess && _dcs.TryGetValue(world.Item2, out var dc))
|
||||
{
|
||||
if (increase)
|
||||
_gauge.WithLabels(dc, world.Item1, gender, race, subrace).Inc();
|
||||
else
|
||||
_gauge.WithLabels(dc, world.Item1, gender, race, subrace).Dec();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using LightlessSyncShared.Metrics;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public class OnlineSyncedPairCacheService
|
||||
{
|
||||
private readonly Dictionary<string, PairCache> _lastSeenCache = new(StringComparer.Ordinal);
|
||||
private readonly SemaphoreSlim _cacheModificationSemaphore = new(1);
|
||||
private readonly ILogger<OnlineSyncedPairCacheService> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMetrics _lightlessMetrics;
|
||||
|
||||
public OnlineSyncedPairCacheService(ILogger<OnlineSyncedPairCacheService> logger, ILoggerFactory loggerFactory, LightlessMetrics lightlessMetrics)
|
||||
{
|
||||
_logger = logger;
|
||||
_loggerFactory = loggerFactory;
|
||||
_lightlessMetrics = lightlessMetrics;
|
||||
}
|
||||
|
||||
public async Task InitPlayer(string user)
|
||||
{
|
||||
if (_lastSeenCache.ContainsKey(user)) return;
|
||||
|
||||
await _cacheModificationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Initializing {user}", user);
|
||||
_lastSeenCache[user] = new(_loggerFactory.CreateLogger<PairCache>(), user, _lightlessMetrics);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cacheModificationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisposePlayer(string user)
|
||||
{
|
||||
if (!_lastSeenCache.ContainsKey(user)) return;
|
||||
|
||||
await _cacheModificationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Disposing {user}", user);
|
||||
_lastSeenCache.Remove(user, out var pairCache);
|
||||
pairCache?.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cacheModificationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AreAllPlayersCached(string sender, List<string> uids, CancellationToken ct)
|
||||
{
|
||||
if (!_lastSeenCache.ContainsKey(sender)) await InitPlayer(sender).ConfigureAwait(false);
|
||||
|
||||
_lastSeenCache.TryGetValue(sender, out var pairCache);
|
||||
return await pairCache.AreAllPlayersCached(uids, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CachePlayers(string sender, List<string> uids, CancellationToken ct)
|
||||
{
|
||||
if (!_lastSeenCache.ContainsKey(sender)) await InitPlayer(sender).ConfigureAwait(false);
|
||||
|
||||
_lastSeenCache.TryGetValue(sender, out var pairCache);
|
||||
await pairCache.CachePlayers(uids, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private sealed class PairCache : IDisposable
|
||||
{
|
||||
private readonly ILogger<PairCache> _logger;
|
||||
private readonly string _owner;
|
||||
private readonly LightlessMetrics _metrics;
|
||||
private readonly Dictionary<string, DateTime> _lastSeenCache = new(StringComparer.Ordinal);
|
||||
private readonly SemaphoreSlim _lock = new(1);
|
||||
|
||||
public PairCache(ILogger<PairCache> logger, string owner, LightlessMetrics metrics)
|
||||
{
|
||||
metrics.IncGauge(MetricsAPI.GaugeUserPairCacheUsers);
|
||||
_logger = logger;
|
||||
_owner = owner;
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
public async Task<bool> AreAllPlayersCached(List<string> uids, CancellationToken ct)
|
||||
{
|
||||
await _lock.WaitAsync(ct).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var allCached = uids.TrueForAll(u => _lastSeenCache.TryGetValue(u, out var expiry) && expiry > DateTime.UtcNow);
|
||||
|
||||
_logger.LogDebug("AreAllPlayersCached:{uid}:{count}:{result}", _owner, uids.Count, allCached);
|
||||
|
||||
if (allCached) _metrics.IncCounter(MetricsAPI.CounterUserPairCacheHit);
|
||||
else _metrics.IncCounter(MetricsAPI.CounterUserPairCacheMiss);
|
||||
|
||||
return allCached;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CachePlayers(List<string> uids, CancellationToken ct)
|
||||
{
|
||||
await _lock.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var lastSeen = DateTime.UtcNow.AddMinutes(60);
|
||||
_logger.LogDebug("CacheOnlinePlayers:{uid}:{count}", _owner, uids.Count);
|
||||
var newEntries = uids.Count(u => !_lastSeenCache.ContainsKey(u));
|
||||
|
||||
_metrics.IncCounter(MetricsAPI.CounterUserPairCacheNewEntries, newEntries);
|
||||
_metrics.IncCounter(MetricsAPI.CounterUserPairCacheUpdatedEntries, uids.Count - newEntries);
|
||||
|
||||
_metrics.IncGauge(MetricsAPI.GaugeUserPairCacheEntries, newEntries);
|
||||
uids.ForEach(u => _lastSeenCache[u] = lastSeen);
|
||||
|
||||
// clean up old entries
|
||||
var outdatedEntries = _lastSeenCache.Where(u => u.Value < DateTime.UtcNow).Select(k => k.Key).ToList();
|
||||
if (outdatedEntries.Any())
|
||||
{
|
||||
_metrics.DecGauge(MetricsAPI.GaugeUserPairCacheEntries, outdatedEntries.Count);
|
||||
foreach (var entry in outdatedEntries)
|
||||
{
|
||||
_lastSeenCache.Remove(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_metrics.DecGauge(MetricsAPI.GaugeUserPairCacheUsers);
|
||||
_metrics.DecGauge(MetricsAPI.GaugeUserPairCacheEntries, _lastSeenCache.Count);
|
||||
_lock.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSyncServer.Hubs;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis.Extensions.Core.Abstractions;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public sealed class SystemInfoService : BackgroundService
|
||||
{
|
||||
private readonly LightlessMetrics _lightlessMetrics;
|
||||
private readonly IConfigurationService<ServerConfiguration> _config;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
|
||||
private readonly ILogger<SystemInfoService> _logger;
|
||||
private readonly IHubContext<LightlessHub, ILightlessHub> _hubContext;
|
||||
private readonly IRedisDatabase _redis;
|
||||
public SystemInfoDto SystemInfoDto { get; private set; } = new();
|
||||
|
||||
public SystemInfoService(LightlessMetrics lightlessMetrics, IConfigurationService<ServerConfiguration> configurationService, IDbContextFactory<LightlessDbContext> dbContextFactory,
|
||||
ILogger<SystemInfoService> logger, IHubContext<LightlessHub, ILightlessHub> hubContext, IRedisDatabase redisDb)
|
||||
{
|
||||
_lightlessMetrics = lightlessMetrics;
|
||||
_config = configurationService;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
_hubContext = hubContext;
|
||||
_redis = redisDb;
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await base.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("System Info Service started");
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
var timeOut = _config.IsMain ? 15 : 30;
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads);
|
||||
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads);
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
|
||||
|
||||
var onlineUsers = (_redis.SearchKeysAsync("UID:*").GetAwaiter().GetResult()).Count();
|
||||
SystemInfoDto = new SystemInfoDto()
|
||||
{
|
||||
OnlineUsers = onlineUsers,
|
||||
};
|
||||
|
||||
if (_config.IsMain)
|
||||
{
|
||||
_logger.LogInformation("Sending System Info, Online Users: {onlineUsers}", onlineUsers);
|
||||
|
||||
await _hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto).ConfigureAwait(false);
|
||||
|
||||
using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
|
||||
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers);
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count());
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.Permissions.AsNoTracking().Where(p => p.IsPaused).Count());
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count());
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.AsNoTracking().Count());
|
||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, db.Users.AsNoTracking().Count());
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(timeOut), ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to push system info");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public class UserCleanupService : IHostedService
|
||||
{
|
||||
private readonly LightlessMetrics metrics;
|
||||
private readonly ILogger<UserCleanupService> _logger;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _lightlessDbContextFactory;
|
||||
private readonly IConfigurationService<ServerConfiguration> _configuration;
|
||||
private CancellationTokenSource _cleanupCts;
|
||||
|
||||
public UserCleanupService(LightlessMetrics metrics, ILogger<UserCleanupService> logger, IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, IConfigurationService<ServerConfiguration> configuration)
|
||||
{
|
||||
this.metrics = metrics;
|
||||
_logger = logger;
|
||||
_lightlessDbContextFactory = lightlessDbContextFactory;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Cleanup Service started");
|
||||
_cleanupCts = new();
|
||||
|
||||
_ = CleanUp(_cleanupCts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CleanUp(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
using (var dbContext = await _lightlessDbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
CleanUpOutdatedLodestoneAuths(dbContext);
|
||||
|
||||
await PurgeUnusedAccounts(dbContext).ConfigureAwait(false);
|
||||
|
||||
await PurgeTempInvites(dbContext).ConfigureAwait(false);
|
||||
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);
|
||||
TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % 10, 0);
|
||||
var span = futureTime.AddMinutes(10) - currentTime;
|
||||
|
||||
_logger.LogInformation("User Cleanup Complete, next run at {date}", now.Add(span));
|
||||
await Task.Delay(span, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PurgeTempInvites(LightlessDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempInvites = await dbContext.GroupTempInvites.ToListAsync().ConfigureAwait(false);
|
||||
dbContext.RemoveRange(tempInvites.Where(i => i.ExpirationDate < DateTime.UtcNow));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during Temp Invite purge");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PurgeUnusedAccounts(LightlessDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccounts), false))
|
||||
{
|
||||
var usersOlderThanDays = _configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccountsPeriodInDays), 14);
|
||||
var maxGroupsByUser = _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3);
|
||||
|
||||
_logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays);
|
||||
|
||||
var allUsers = dbContext.Users.Where(u => string.IsNullOrEmpty(u.Alias)).ToList();
|
||||
List<User> usersToRemove = new();
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
if (user.LastLoggedIn < DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays))
|
||||
{
|
||||
_logger.LogInformation("User outdated: {userUID}", user.UID);
|
||||
usersToRemove.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var user in usersToRemove)
|
||||
{
|
||||
await SharedDbFunctions.PurgeUser(_logger, user, dbContext, maxGroupsByUser).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during user purge");
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpOutdatedLodestoneAuths(LightlessDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Cleaning up expired lodestone authentications");
|
||||
var lodestoneAuths = dbContext.LodeStoneAuth.Include(u => u.User).Where(a => a.StartedAt != null).ToList();
|
||||
List<LodeStoneAuth> expiredAuths = new List<LodeStoneAuth>();
|
||||
foreach (var auth in lodestoneAuths)
|
||||
{
|
||||
if (auth.StartedAt < DateTime.UtcNow - TimeSpan.FromMinutes(15))
|
||||
{
|
||||
expiredAuths.Add(auth);
|
||||
}
|
||||
}
|
||||
|
||||
dbContext.Users.RemoveRange(expiredAuths.Where(u => u.User != null).Select(a => a.User));
|
||||
dbContext.RemoveRange(expiredAuths);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during expired auths cleanup");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PurgeUser(User user, LightlessDbContext dbContext)
|
||||
{
|
||||
_logger.LogInformation("Purging user: {uid}", user.UID);
|
||||
|
||||
var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
dbContext.Remove(lodestone);
|
||||
}
|
||||
|
||||
var auth = dbContext.Auth.Single(a => a.UserUID == user.UID);
|
||||
|
||||
var userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList();
|
||||
dbContext.Files.RemoveRange(userFiles);
|
||||
|
||||
var ownPairData = dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToList();
|
||||
dbContext.ClientPairs.RemoveRange(ownPairData);
|
||||
var otherPairData = dbContext.ClientPairs.Include(u => u.User)
|
||||
.Where(u => u.OtherUser.UID == user.UID).ToList();
|
||||
dbContext.ClientPairs.RemoveRange(otherPairData);
|
||||
|
||||
var userJoinedGroups = await dbContext.GroupPairs.Include(g => g.Group).Where(u => u.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var userGroupPair in userJoinedGroups)
|
||||
{
|
||||
bool ownerHasLeft = string.Equals(userGroupPair.Group.OwnerUID, user.UID, StringComparison.Ordinal);
|
||||
|
||||
if (ownerHasLeft)
|
||||
{
|
||||
var groupPairs = await dbContext.GroupPairs.Where(g => g.GroupGID == userGroupPair.GroupGID && g.GroupUserUID != user.UID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
if (!groupPairs.Any())
|
||||
{
|
||||
_logger.LogInformation("Group {gid} has no new owner, deleting", userGroupPair.GroupGID);
|
||||
dbContext.Groups.Remove(userGroupPair.Group);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = await SharedDbFunctions.MigrateOrDeleteGroup(dbContext, userGroupPair.Group, groupPairs, _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
dbContext.GroupPairs.Remove(userGroupPair);
|
||||
}
|
||||
|
||||
_logger.LogInformation("User purged: {uid}", user.UID);
|
||||
|
||||
dbContext.Auth.Remove(auth);
|
||||
dbContext.Users.Remove(user);
|
||||
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cleanupCts.Cancel();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
367
LightlessSyncServer/LightlessSyncServer/Startup.cs
Normal file
367
LightlessSyncServer/LightlessSyncServer/Startup.cs
Normal file
@@ -0,0 +1,367 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LightlessSyncServer.Hubs;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using AspNetCoreRateLimit;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncServer.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Services;
|
||||
using Prometheus;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using StackExchange.Redis;
|
||||
using StackExchange.Redis.Extensions.Core.Configuration;
|
||||
using System.Net;
|
||||
using StackExchange.Redis.Extensions.System.Text.Json;
|
||||
using LightlessSync.API.SignalR;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using LightlessSyncServer.Controllers;
|
||||
using LightlessSyncShared.RequirementHandlers;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
namespace LightlessSyncServer;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
private readonly ILogger<Startup> _logger;
|
||||
|
||||
public Startup(IConfiguration configuration, ILogger<Startup> logger)
|
||||
{
|
||||
Configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddTransient(_ => Configuration);
|
||||
|
||||
var lightlessConfig = Configuration.GetRequiredSection("LightlessSync");
|
||||
|
||||
// configure metrics
|
||||
ConfigureMetrics(services);
|
||||
|
||||
// configure database
|
||||
ConfigureDatabase(services, lightlessConfig);
|
||||
|
||||
// configure authentication and authorization
|
||||
ConfigureAuthorization(services);
|
||||
|
||||
// configure rate limiting
|
||||
ConfigureIpRateLimiting(services);
|
||||
|
||||
// configure SignalR
|
||||
ConfigureSignalR(services, lightlessConfig);
|
||||
|
||||
// configure lightless specific services
|
||||
ConfigureLightlessServices(services, lightlessConfig);
|
||||
|
||||
services.AddHealthChecks();
|
||||
services.AddControllers().ConfigureApplicationPartManager(a =>
|
||||
{
|
||||
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
|
||||
if (lightlessConfig.GetValue<Uri>(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null)
|
||||
{
|
||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(LightlessServerConfigurationController), typeof(LightlessBaseConfigurationController), typeof(ClientMessageController)));
|
||||
}
|
||||
else
|
||||
{
|
||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureLightlessServices(IServiceCollection services, IConfigurationSection lightlessConfig)
|
||||
{
|
||||
bool isMainServer = lightlessConfig.GetValue<Uri>(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null;
|
||||
|
||||
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
services.Configure<LightlessConfigurationBase>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
|
||||
services.AddSingleton<ServerTokenGenerator>();
|
||||
services.AddSingleton<SystemInfoService>();
|
||||
services.AddSingleton<OnlineSyncedPairCacheService>();
|
||||
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
|
||||
// configure services based on main server status
|
||||
ConfigureServicesBasedOnShardType(services, lightlessConfig, isMainServer);
|
||||
|
||||
services.AddSingleton(s => new LightlessCensus(s.GetRequiredService<ILogger<LightlessCensus>>()));
|
||||
services.AddHostedService(p => p.GetRequiredService<LightlessCensus>());
|
||||
|
||||
if (isMainServer)
|
||||
{
|
||||
services.AddSingleton<UserCleanupService>();
|
||||
services.AddHostedService(provider => provider.GetService<UserCleanupService>());
|
||||
services.AddSingleton<CharaDataCleanupService>();
|
||||
services.AddHostedService(provider => provider.GetService<CharaDataCleanupService>());
|
||||
services.AddHostedService<ClientPairPermissionsCleanupService>();
|
||||
}
|
||||
|
||||
services.AddSingleton<GPoseLobbyDistributionService>();
|
||||
services.AddHostedService(provider => provider.GetService<GPoseLobbyDistributionService>());
|
||||
}
|
||||
|
||||
private static void ConfigureSignalR(IServiceCollection services, IConfigurationSection lightlessConfig)
|
||||
{
|
||||
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
||||
services.AddSingleton<ConcurrencyFilter>();
|
||||
|
||||
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
|
||||
{
|
||||
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
|
||||
hubOptions.EnableDetailedErrors = true;
|
||||
hubOptions.MaximumParallelInvocationsPerClient = 10;
|
||||
hubOptions.StreamBufferCapacity = 200;
|
||||
|
||||
hubOptions.AddFilter<SignalRLimitFilter>();
|
||||
hubOptions.AddFilter<ConcurrencyFilter>();
|
||||
}).AddMessagePackProtocol(opt =>
|
||||
{
|
||||
var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance,
|
||||
BuiltinResolver.Instance,
|
||||
AttributeFormatterResolver.Instance,
|
||||
// replace enum resolver
|
||||
DynamicEnumAsStringResolver.Instance,
|
||||
DynamicGenericResolver.Instance,
|
||||
DynamicUnionResolver.Instance,
|
||||
DynamicObjectResolver.Instance,
|
||||
PrimitiveObjectResolver.Instance,
|
||||
// final fallback(last priority)
|
||||
StandardResolver.Instance);
|
||||
|
||||
opt.SerializerOptions = MessagePackSerializerOptions.Standard
|
||||
.WithCompression(MessagePackCompression.Lz4Block)
|
||||
.WithResolver(resolver);
|
||||
});
|
||||
|
||||
|
||||
// configure redis for SignalR
|
||||
var redisConnection = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
|
||||
signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { });
|
||||
|
||||
var options = ConfigurationOptions.Parse(redisConnection);
|
||||
|
||||
var endpoint = options.EndPoints[0];
|
||||
string address = "";
|
||||
int port = 0;
|
||||
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; }
|
||||
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; }
|
||||
var redisConfiguration = new RedisConfiguration()
|
||||
{
|
||||
AbortOnConnectFail = true,
|
||||
KeyPrefix = "",
|
||||
Hosts = new RedisHost[]
|
||||
{
|
||||
new RedisHost(){ Host = address, Port = port },
|
||||
},
|
||||
AllowAdmin = true,
|
||||
ConnectTimeout = options.ConnectTimeout,
|
||||
Database = 0,
|
||||
Ssl = false,
|
||||
Password = options.Password,
|
||||
ServerEnumerationStrategy = new ServerEnumerationStrategy()
|
||||
{
|
||||
Mode = ServerEnumerationStrategy.ModeOptions.All,
|
||||
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
|
||||
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw,
|
||||
},
|
||||
MaxValueLength = 1024,
|
||||
PoolSize = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
|
||||
SyncTimeout = options.SyncTimeout,
|
||||
};
|
||||
|
||||
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);
|
||||
}
|
||||
|
||||
private void ConfigureIpRateLimiting(IServiceCollection services)
|
||||
{
|
||||
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
|
||||
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
|
||||
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
services.AddMemoryCache();
|
||||
services.AddInMemoryRateLimiting();
|
||||
}
|
||||
|
||||
private static void ConfigureAuthorization(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
|
||||
services.AddTransient<IAuthorizationHandler, ValidTokenRequirementHandler>();
|
||||
services.AddTransient<IAuthorizationHandler, ValidTokenHubRequirementHandler>();
|
||||
|
||||
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
||||
.Configure<IConfigurationService<LightlessConfigurationBase>>((options, config) =>
|
||||
{
|
||||
options.TokenValidationParameters = new()
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(LightlessConfigurationBase.Jwt)))),
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.DefaultPolicy = new AuthorizationPolicyBuilder()
|
||||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser().Build();
|
||||
options.AddPolicy("Authenticated", policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
});
|
||||
options.AddPolicy("Identified", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified));
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
|
||||
});
|
||||
options.AddPolicy("Admin", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator));
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
|
||||
});
|
||||
options.AddPolicy("Moderator", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator));
|
||||
policy.AddRequirements(new ValidTokenRequirement());
|
||||
});
|
||||
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build());
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureDatabase(IServiceCollection services, IConfigurationSection lightlessConfig)
|
||||
{
|
||||
services.AddDbContextPool<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("LightlessSyncShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, lightlessConfig.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024));
|
||||
services.AddDbContextFactory<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("LightlessSyncShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureMetrics(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<LightlessMetrics>(m => new LightlessMetrics(m.GetService<ILogger<LightlessMetrics>>(), new List<string>
|
||||
{
|
||||
MetricsAPI.CounterInitializedConnections,
|
||||
MetricsAPI.CounterUserPushData,
|
||||
MetricsAPI.CounterUserPushDataTo,
|
||||
MetricsAPI.CounterUsersRegisteredDeleted,
|
||||
MetricsAPI.CounterAuthenticationCacheHits,
|
||||
MetricsAPI.CounterAuthenticationFailures,
|
||||
MetricsAPI.CounterAuthenticationRequests,
|
||||
MetricsAPI.CounterAuthenticationSuccesses,
|
||||
MetricsAPI.CounterUserPairCacheHit,
|
||||
MetricsAPI.CounterUserPairCacheMiss,
|
||||
MetricsAPI.CounterUserPairCacheNewEntries,
|
||||
MetricsAPI.CounterUserPairCacheUpdatedEntries,
|
||||
}, new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeAuthorizedConnections,
|
||||
MetricsAPI.GaugeConnections,
|
||||
MetricsAPI.GaugePairs,
|
||||
MetricsAPI.GaugePairsPaused,
|
||||
MetricsAPI.GaugeAvailableIOWorkerThreads,
|
||||
MetricsAPI.GaugeAvailableWorkerThreads,
|
||||
MetricsAPI.GaugeGroups,
|
||||
MetricsAPI.GaugeGroupPairs,
|
||||
MetricsAPI.GaugeUsersRegistered,
|
||||
MetricsAPI.GaugeAuthenticationCacheEntries,
|
||||
MetricsAPI.GaugeUserPairCacheEntries,
|
||||
MetricsAPI.GaugeUserPairCacheUsers,
|
||||
MetricsAPI.GaugeGposeLobbies,
|
||||
MetricsAPI.GaugeGposeLobbyUsers,
|
||||
MetricsAPI.GaugeHubConcurrency,
|
||||
MetricsAPI.GaugeHubQueuedConcurrency,
|
||||
}));
|
||||
}
|
||||
|
||||
private static void ConfigureServicesBasedOnShardType(IServiceCollection services, IConfigurationSection lightlessConfig, bool isMainServer)
|
||||
{
|
||||
if (!isMainServer)
|
||||
{
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, LightlessConfigurationServiceClient<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
||||
|
||||
services.AddHostedService(p => (LightlessConfigurationServiceClient<ServerConfiguration>)p.GetService<IConfigurationService<ServerConfiguration>>());
|
||||
services.AddHostedService(p => (LightlessConfigurationServiceClient<LightlessConfigurationBase>)p.GetService<IConfigurationService<LightlessConfigurationBase>>());
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, LightlessConfigurationServiceServer<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceServer<LightlessConfigurationBase>>();
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
|
||||
{
|
||||
logger.LogInformation("Running Configure");
|
||||
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<LightlessConfigurationBase>>();
|
||||
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseHttpMetrics();
|
||||
|
||||
var metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(LightlessConfigurationBase.MetricsPort), 4980));
|
||||
metricServer.Start();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapHub<LightlessHub>(ILightlessHub.Path, options =>
|
||||
{
|
||||
options.ApplicationMaxBufferSize = 5242880;
|
||||
options.TransportMaxBufferSize = 5242880;
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
|
||||
});
|
||||
|
||||
endpoints.MapHealthChecks("/health").AllowAnonymous();
|
||||
endpoints.MapControllers();
|
||||
|
||||
foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>())
|
||||
{
|
||||
if (source == null) continue;
|
||||
_logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
74
LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs
Normal file
74
LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSyncShared.Models;
|
||||
using static LightlessSyncServer.Hubs.LightlessHub;
|
||||
|
||||
namespace LightlessSyncServer.Utils;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static GroupData ToGroupData(this Group group)
|
||||
{
|
||||
return new GroupData(group.GID, group.Alias);
|
||||
}
|
||||
|
||||
public static UserData ToUserData(this GroupPair pair)
|
||||
{
|
||||
return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias);
|
||||
}
|
||||
|
||||
public static UserData ToUserData(this User user)
|
||||
{
|
||||
return new UserData(user.UID, user.Alias);
|
||||
}
|
||||
|
||||
public static IndividualPairStatus ToIndividualPairStatus(this UserInfo userInfo)
|
||||
{
|
||||
if (userInfo.IndividuallyPaired) return IndividualPairStatus.Bidirectional;
|
||||
if (!userInfo.IndividuallyPaired && userInfo.GIDs.Contains(Constants.IndividualKeyword, StringComparer.Ordinal)) return IndividualPairStatus.OneSided;
|
||||
return IndividualPairStatus.None;
|
||||
}
|
||||
|
||||
public static GroupPermissions ToEnum(this Group group)
|
||||
{
|
||||
var permissions = GroupPermissions.NoneSet;
|
||||
permissions.SetPreferDisableAnimations(group.PreferDisableAnimations);
|
||||
permissions.SetPreferDisableSounds(group.PreferDisableSounds);
|
||||
permissions.SetPreferDisableVFX(group.PreferDisableVFX);
|
||||
permissions.SetDisableInvites(!group.InvitesEnabled);
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public static GroupUserPreferredPermissions ToEnum(this GroupPairPreferredPermission groupPair)
|
||||
{
|
||||
var permissions = GroupUserPreferredPermissions.NoneSet;
|
||||
permissions.SetDisableAnimations(groupPair.DisableAnimations);
|
||||
permissions.SetDisableSounds(groupPair.DisableSounds);
|
||||
permissions.SetPaused(groupPair.IsPaused);
|
||||
permissions.SetDisableVFX(groupPair.DisableVFX);
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public static GroupPairUserInfo ToEnum(this GroupPair groupPair)
|
||||
{
|
||||
var groupUserInfo = GroupPairUserInfo.None;
|
||||
groupUserInfo.SetPinned(groupPair.IsPinned);
|
||||
groupUserInfo.SetModerator(groupPair.IsModerator);
|
||||
return groupUserInfo;
|
||||
}
|
||||
|
||||
public static UserPermissions ToUserPermissions(this UserPermissionSet? permissions, bool setSticky = false)
|
||||
{
|
||||
if (permissions == null) return UserPermissions.NoneSet;
|
||||
|
||||
UserPermissions perm = UserPermissions.NoneSet;
|
||||
perm.SetPaused(permissions.IsPaused);
|
||||
perm.SetDisableAnimations(permissions.DisableAnimations);
|
||||
perm.SetDisableSounds(permissions.DisableSounds);
|
||||
perm.SetDisableVFX(permissions.DisableVFX);
|
||||
if (setSticky)
|
||||
perm.SetSticky(permissions.Sticky);
|
||||
return perm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSyncServer.Hubs;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace LightlessSyncServer.Utils;
|
||||
|
||||
public class LightlessHubLogger
|
||||
{
|
||||
private readonly LightlessHub _hub;
|
||||
private readonly ILogger<LightlessHub> _logger;
|
||||
|
||||
public LightlessHubLogger(LightlessHub hub, ILogger<LightlessHub> logger)
|
||||
{
|
||||
_hub = hub;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static object[] Args(params object[] args)
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
public void LogCallInfo(object[] args = null, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
string formattedArgs = args != null && args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogInformation("{uid}:{method}{args}", _hub.UserUID, methodName, formattedArgs);
|
||||
}
|
||||
|
||||
public void LogCallWarning(object[] args = null, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
string formattedArgs = args != null && args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogWarning("{uid}:{method}{args}", _hub.UserUID, methodName, formattedArgs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace LightlessSyncServer.Utils;
|
||||
|
||||
public enum PauseInfo
|
||||
{
|
||||
NoConnection,
|
||||
Paused,
|
||||
Unpaused,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace LightlessSyncServer.Utils;
|
||||
|
||||
public record PauseState
|
||||
{
|
||||
public string GID { get; set; }
|
||||
public bool IsPaused => IsSelfPaused || IsOtherPaused;
|
||||
public bool IsSelfPaused { get; set; }
|
||||
public bool IsOtherPaused { get; set; }
|
||||
}
|
||||
58
LightlessSyncServer/LightlessSyncServer/Utils/PausedEntry.cs
Normal file
58
LightlessSyncServer/LightlessSyncServer/Utils/PausedEntry.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace LightlessSyncServer.Utils;
|
||||
|
||||
public record PausedEntry
|
||||
{
|
||||
public string UID { get; set; }
|
||||
public List<PauseState> PauseStates { get; set; } = new();
|
||||
|
||||
public PauseInfo IsDirectlyPaused => PauseStateWithoutGroups == null ? PauseInfo.NoConnection
|
||||
: PauseStates.First(g => g.GID == null).IsPaused ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
|
||||
public PauseInfo IsPausedPerGroup => !PauseStatesWithoutDirect.Any() ? PauseInfo.NoConnection
|
||||
: PauseStatesWithoutDirect.All(p => p.IsPaused) ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
|
||||
private IEnumerable<PauseState> PauseStatesWithoutDirect => PauseStates.Where(f => f.GID != null);
|
||||
private PauseState PauseStateWithoutGroups => PauseStates.SingleOrDefault(p => p.GID == null);
|
||||
|
||||
public bool IsPaused
|
||||
{
|
||||
get
|
||||
{
|
||||
var isDirectlyPaused = IsDirectlyPaused;
|
||||
bool result;
|
||||
if (isDirectlyPaused != PauseInfo.NoConnection)
|
||||
{
|
||||
result = isDirectlyPaused == PauseInfo.Paused;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = IsPausedPerGroup == PauseInfo.Paused;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public PauseInfo IsOtherPausedForSpecificGroup(string gid)
|
||||
{
|
||||
var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal));
|
||||
if (state == null) return PauseInfo.NoConnection;
|
||||
return state.IsOtherPaused ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
}
|
||||
|
||||
public PauseInfo IsPausedForSpecificGroup(string gid)
|
||||
{
|
||||
var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal));
|
||||
if (state == null) return PauseInfo.NoConnection;
|
||||
return state.IsPaused ? PauseInfo.Paused : PauseInfo.NoConnection;
|
||||
}
|
||||
|
||||
public PauseInfo IsPausedExcludingGroup(string gid)
|
||||
{
|
||||
var states = PauseStatesWithoutDirect.Where(f => !string.Equals(f.GID, gid, StringComparison.Ordinal)).ToList();
|
||||
if (!states.Any()) return PauseInfo.NoConnection;
|
||||
var result = states.All(p => p.IsPaused);
|
||||
if (result) return PauseInfo.Paused;
|
||||
return PauseInfo.Unpaused;
|
||||
}
|
||||
}
|
||||
12
LightlessSyncServer/LightlessSyncServer/Utils/UserPair.cs
Normal file
12
LightlessSyncServer/LightlessSyncServer/Utils/UserPair.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace LightlessSyncServer.Hubs;
|
||||
|
||||
public partial class LightlessHub
|
||||
{
|
||||
private record UserPair
|
||||
{
|
||||
public string UserUID { get; set; }
|
||||
public string OtherUserUID { get; set; }
|
||||
public bool UserPausedOther { get; set; }
|
||||
public bool OtherPausedUser { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"DetailedErrors": true,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
61
LightlessSyncServer/LightlessSyncServer/appsettings.json
Normal file
61
LightlessSyncServer/LightlessSyncServer/appsettings.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=;Username=;Password="
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"LightlessSyncServer.Authentication": "Warning",
|
||||
"System.IO.IOException": "Warning"
|
||||
},
|
||||
"File": {
|
||||
"BasePath": "logs",
|
||||
"FileAccessMode": "KeepOpenAndAutoFlush",
|
||||
"FileEncodingName": "utf-8",
|
||||
"DateFormat": "yyyMMdd",
|
||||
"MaxFileSize": 10485760,
|
||||
"Files": [
|
||||
{
|
||||
"Path": "lightless-<counter>.log"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"LightlessSync": {
|
||||
"DbContextPoolSize": 2000,
|
||||
"CdnFullUrl": "http://localhost/cache/",
|
||||
"ServiceAddress": "http://localhost:5002",
|
||||
"StaticFileServiceAddress": "http://localhost:5003"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://+:6000",
|
||||
"Certificate": {
|
||||
"Subject": "sync.lightless-sync.org",
|
||||
"Store": "My",
|
||||
"Location": "LocalMachine"
|
||||
//"AllowInvalid": false
|
||||
// "Path": "", //use path, keypath and password to provide a valid certificate if not using windows key store
|
||||
// "KeyPath": ""
|
||||
// "Password": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"IpRateLimiting": {
|
||||
"EnableEndpointRateLimiting": false,
|
||||
"StackBlockedRequests": false,
|
||||
"RealIpHeader": "X-Real-IP",
|
||||
"ClientIdHeader": "X-ClientId",
|
||||
"HttpStatusCode": 429,
|
||||
"IpWhitelist": [ ],
|
||||
"GeneralRules": [ ]
|
||||
},
|
||||
"IPRateLimitPolicies": {
|
||||
"IpRules": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using FluentAssertions;
|
||||
using LightlessSyncServer.Discord;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LightlessSyncServerTest.Discord {
|
||||
public class DiscordBotTest {
|
||||
|
||||
[Test]
|
||||
[TestCase("", null)]
|
||||
[TestCase("abcd", null)]
|
||||
[TestCase("www.google.de", null)]
|
||||
[TestCase("https://www.google.de", null)]
|
||||
[TestCase("de.finalfantasyxiv.com/lodestone/character/1234", null)]
|
||||
[TestCase("https://cn.finalfantasyxiv.com/lodestone/character/1234", null)]
|
||||
[TestCase("http://jp.finalfantasyxiv.com/lodestone/character/1234", null)]
|
||||
[TestCase("https://jp.finalfantasyxiv.com/character/1234", null)]
|
||||
[TestCase("https://jp.finalfantasyxiv.com/lodestone/1234", null)]
|
||||
[TestCase("https://www.finalfantasyxiv.com/lodestone/character/1234", null)]
|
||||
[TestCase("https://jp.finalfantasyxiv.com/lodestone/character/1234", 1234)]
|
||||
[TestCase("https://fr.finalfantasyxiv.com/lodestone/character/1234", 1234)]
|
||||
[TestCase("https://eu.finalfantasyxiv.com/lodestone/character/1234/", 1234)]
|
||||
[TestCase("https://eu.finalfantasyxiv.com/lodestone/character/1234?myurlparameter=500", 1234)]
|
||||
[TestCase("https://de.finalfantasyxiv.com/lodestone/character/1234/whatever/3456", 1234)]
|
||||
[TestCase("https://na.finalfantasyxiv.com/lodestone/character/1234abcd4321/whatever/3456", 1234)]
|
||||
public void ParseCharacterIdFromLodestoneUrl_CheckThatIdIsParsedCorrectly(string url, int? expectedId) {
|
||||
var inMemorySettings = new Dictionary<string, string> {
|
||||
{"DiscordBotToken", "1234"}
|
||||
};
|
||||
|
||||
IConfiguration configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(inMemorySettings)
|
||||
.Build();
|
||||
|
||||
var spMock = new Mock<IServiceProvider>();
|
||||
var loggerMock = new Mock<ILogger<DiscordBot>>();
|
||||
|
||||
var sut = new DiscordBot(spMock.Object, configuration, loggerMock.Object);
|
||||
MethodInfo methodInfo = sut.GetType().GetMethod("ParseCharacterIdFromLodestoneUrl", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var actualId = methodInfo.Invoke(sut, new object[] { url });
|
||||
|
||||
actualId.Should().Be(expectedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using LightlessSyncServer.Hubs;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
|
||||
namespace LightlessSyncServerTest.Hubs {
|
||||
public class LightlessHubTest {
|
||||
|
||||
[Test]
|
||||
public async Task Disconnect_QueryReturnsCorrectResult_Test() {
|
||||
var options = new DbContextOptionsBuilder<LightlessDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: "lightless").Options;
|
||||
|
||||
using var context = new LightlessDbContext(options);
|
||||
context.Users.Add(new User() { UID = "User1", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident1" });
|
||||
context.Users.Add(new User() { UID = "User2", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident2" });
|
||||
context.Users.Add(new User() { UID = "User3", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident3" });
|
||||
context.Users.Add(new User() { UID = "User4", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident4" });
|
||||
context.Users.Add(new User() { UID = "User5", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident5" });
|
||||
context.Users.Add(new User() { UID = "User6", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident6" });
|
||||
|
||||
// Valid pairs
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User2", IsPaused = false });
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User2", OtherUserUID = "User1", IsPaused = false });
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User3", IsPaused = false });
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User3", OtherUserUID = "User1", IsPaused = false });
|
||||
|
||||
// Other user paired but user not paired with current user
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User4", OtherUserUID = "User1", IsPaused = false });
|
||||
|
||||
// Valid pair but user paused
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User5", IsPaused = true });
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User5", OtherUserUID = "User1", IsPaused = false });
|
||||
|
||||
// Valid pair but other user paused
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User6", IsPaused = false });
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User6", OtherUserUID = "User1", IsPaused = true });
|
||||
|
||||
// Non existant user
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User99", OtherUserUID = "User1", IsPaused = false });
|
||||
|
||||
// Non-related data
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User6", OtherUserUID = "User4", IsPaused = true });
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User4", OtherUserUID = "User3", IsPaused = false });
|
||||
context.ClientPairs.Add(new ClientPair() { UserUID = "User3", OtherUserUID = "User2", IsPaused = false });
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
var clientContextMock = new Mock<HubCallerContext>();
|
||||
var claimMock = new Mock<ClaimsPrincipal>();
|
||||
var claim = new Claim(ClaimTypes.NameIdentifier, "User1");
|
||||
claimMock.SetupGet(x => x.Claims).Returns(new List<Claim>() { claim });
|
||||
clientContextMock.SetupGet(x => x.User).Returns(claimMock.Object);
|
||||
|
||||
var clientsMock = new Mock<IHubCallerClients>();
|
||||
var clientProxyMock = new Mock<IClientProxy>();
|
||||
clientsMock.Setup(x => x.Users(It.IsAny<IReadOnlyList<string>>())).Returns(clientProxyMock.Object);
|
||||
|
||||
var hub = new LightlessHub(context, new Mock<ILogger<LightlessHub>>().Object, null, new Mock<IConfiguration>().Object, new Mock<IHttpContextAccessor>().Object);
|
||||
|
||||
|
||||
hub.Clients = clientsMock.Object;
|
||||
hub.Context = clientContextMock.Object;
|
||||
|
||||
await hub.OnDisconnectedAsync(new Exception("Test Exception")).ConfigureAwait(false);
|
||||
|
||||
clientsMock.Verify(x => x.Users(It.Is<IReadOnlyList<string>>(x => x.Count() == 2 && x[0] == "User2" && x[1] == "User3")), Times.Once);
|
||||
clientProxyMock.Verify(x => x.SendCoreAsync(It.IsAny<string>(), It.Is<object[]>(o => (string)o[0] == "Ident1"), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LightlessSyncServer\LightlessSyncServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
1
LightlessSyncServer/LightlessSyncServerTest/Usings.cs
Normal file
1
LightlessSyncServer/LightlessSyncServerTest/Usings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using NUnit.Framework;
|
||||
434
LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs
Normal file
434
LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs
Normal file
@@ -0,0 +1,434 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
internal class DiscordBot : IHostedService
|
||||
{
|
||||
private readonly DiscordBotServices _botServices;
|
||||
private readonly IConfigurationService<ServicesConfiguration> _configurationService;
|
||||
private readonly IConnectionMultiplexer _connectionMultiplexer;
|
||||
private readonly DiscordSocketClient _discordClient;
|
||||
private readonly ILogger<DiscordBot> _logger;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
|
||||
private readonly IServiceProvider _services;
|
||||
private InteractionService _interactionModule;
|
||||
private readonly CancellationTokenSource? _processReportQueueCts;
|
||||
private CancellationTokenSource? _clientConnectedCts;
|
||||
|
||||
public DiscordBot(DiscordBotServices botServices, IServiceProvider services, IConfigurationService<ServicesConfiguration> configuration,
|
||||
IDbContextFactory<LightlessDbContext> dbContextFactory,
|
||||
ILogger<DiscordBot> logger, IConnectionMultiplexer connectionMultiplexer)
|
||||
{
|
||||
_botServices = botServices;
|
||||
_services = services;
|
||||
_configurationService = configuration;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
_connectionMultiplexer = connectionMultiplexer;
|
||||
_discordClient = new(new DiscordSocketConfig()
|
||||
{
|
||||
DefaultRetryMode = RetryMode.AlwaysRetry,
|
||||
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers
|
||||
});
|
||||
|
||||
_discordClient.Log += Log;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var token = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty);
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogInformation("Starting DiscordBot");
|
||||
_logger.LogInformation("Using Configuration: " + _configurationService.ToString());
|
||||
|
||||
_interactionModule?.Dispose();
|
||||
_interactionModule = new InteractionService(_discordClient);
|
||||
_interactionModule.Log += Log;
|
||||
await _interactionModule.AddModuleAsync(typeof(LightlessModule), _services).ConfigureAwait(false);
|
||||
await _interactionModule.AddModuleAsync(typeof(LightlessWizardModule), _services).ConfigureAwait(false);
|
||||
|
||||
await _discordClient.LoginAsync(TokenType.Bot, token).ConfigureAwait(false);
|
||||
await _discordClient.StartAsync().ConfigureAwait(false);
|
||||
|
||||
_discordClient.Ready += DiscordClient_Ready;
|
||||
_discordClient.InteractionCreated += async (x) =>
|
||||
{
|
||||
var ctx = new SocketInteractionContext(_discordClient, x);
|
||||
await _interactionModule.ExecuteCommandAsync(ctx, _services).ConfigureAwait(false);
|
||||
};
|
||||
_discordClient.UserJoined += OnUserJoined;
|
||||
|
||||
await _botServices.Start().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnUserJoined(SocketGuildUser arg)
|
||||
{
|
||||
try
|
||||
{
|
||||
using LightlessDbContext dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
var alreadyRegistered = await dbContext.LodeStoneAuth.AnyAsync(u => u.DiscordId == arg.Id).ConfigureAwait(false);
|
||||
if (alreadyRegistered)
|
||||
{
|
||||
await _botServices.AddRegisteredRoleAsync(arg).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to set user role on join");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty)))
|
||||
{
|
||||
await _botServices.Stop().ConfigureAwait(false);
|
||||
_processReportQueueCts?.Cancel();
|
||||
_clientConnectedCts?.Cancel();
|
||||
|
||||
await _discordClient.LogoutAsync().ConfigureAwait(false);
|
||||
await _discordClient.StopAsync().ConfigureAwait(false);
|
||||
_interactionModule?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DiscordClient_Ready()
|
||||
{
|
||||
var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First();
|
||||
await _interactionModule.RegisterCommandsToGuildAsync(guild.Id, true).ConfigureAwait(false);
|
||||
_clientConnectedCts?.Cancel();
|
||||
_clientConnectedCts?.Dispose();
|
||||
_clientConnectedCts = new();
|
||||
_ = UpdateStatusAsync(_clientConnectedCts.Token);
|
||||
|
||||
await CreateOrUpdateModal(guild).ConfigureAwait(false);
|
||||
_botServices.UpdateGuild(guild);
|
||||
await _botServices.LogToChannel("Bot startup complete.").ConfigureAwait(false);
|
||||
_ = UpdateVanityRoles(guild, _clientConnectedCts.Token);
|
||||
_ = RemoveUsersNotInVanityRole(_clientConnectedCts.Token);
|
||||
_ = RemoveUnregisteredUsers(_clientConnectedCts.Token);
|
||||
}
|
||||
|
||||
private async Task UpdateVanityRoles(RestGuild guild, CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Updating Vanity Roles");
|
||||
Dictionary<ulong, string> vanityRoles = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.VanityRoles), new Dictionary<ulong, string>());
|
||||
if (vanityRoles.Keys.Count != _botServices.VanityRoles.Count)
|
||||
{
|
||||
_botServices.VanityRoles.Clear();
|
||||
foreach (var role in vanityRoles)
|
||||
{
|
||||
_logger.LogInformation("Adding Role: {id} => {desc}", role.Key, role.Value);
|
||||
|
||||
var restrole = guild.GetRole(role.Key);
|
||||
if (restrole != null)
|
||||
_botServices.VanityRoles[restrole] = role.Value;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during UpdateVanityRoles");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateOrUpdateModal(RestGuild guild)
|
||||
{
|
||||
_logger.LogInformation("Creating Wizard: Getting Channel");
|
||||
|
||||
var discordChannelForCommands = _configurationService.GetValue<ulong?>(nameof(ServicesConfiguration.DiscordChannelForCommands));
|
||||
if (discordChannelForCommands == null)
|
||||
{
|
||||
_logger.LogWarning("Creating Wizard: No channel configured");
|
||||
return;
|
||||
}
|
||||
|
||||
IUserMessage? message = null;
|
||||
var socketchannel = await _discordClient.GetChannelAsync(discordChannelForCommands.Value).ConfigureAwait(false) as SocketTextChannel;
|
||||
var pinnedMessages = await socketchannel.GetPinnedMessagesAsync().ConfigureAwait(false);
|
||||
foreach (var msg in pinnedMessages)
|
||||
{
|
||||
_logger.LogInformation("Creating Wizard: Checking message id {id}, author is: {author}, hasEmbeds: {embeds}", msg.Id, msg.Author.Id, msg.Embeds.Any());
|
||||
if (msg.Author.Id == _discordClient.CurrentUser.Id
|
||||
&& msg.Embeds.Any())
|
||||
{
|
||||
message = await socketchannel.GetMessageAsync(msg.Id).ConfigureAwait(false) as IUserMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Creating Wizard: Found message id: {id}", message?.Id ?? 0);
|
||||
|
||||
await GenerateOrUpdateWizardMessage(socketchannel, message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task GenerateOrUpdateWizardMessage(SocketTextChannel channel, IUserMessage? prevMessage)
|
||||
{
|
||||
EmbedBuilder eb = new EmbedBuilder();
|
||||
eb.WithTitle("Lightless Services Bot Interaction Service");
|
||||
eb.WithDescription("Press \"Start\" to interact with this bot!" + Environment.NewLine + Environment.NewLine
|
||||
+ "You can handle all of your Lightless account needs in this server through the easy to use interactive bot prompt. Just follow the instructions!");
|
||||
eb.WithThumbnailUrl("https://raw.githubusercontent.com/Light-Public-Syncshells/LightlessSync/refs/heads/main/LightlessSync/icon.png");
|
||||
var cb = new ComponentBuilder();
|
||||
cb.WithButton("Start", style: ButtonStyle.Primary, customId: "wizard-captcha:true", emote: Emoji.Parse("➡️"));
|
||||
if (prevMessage == null)
|
||||
{
|
||||
var msg = await channel.SendMessageAsync(embed: eb.Build(), components: cb.Build()).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await msg.PinAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await prevMessage.ModifyAsync(p =>
|
||||
{
|
||||
p.Embed = eb.Build();
|
||||
p.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Task Log(LogMessage msg)
|
||||
{
|
||||
switch (msg.Severity)
|
||||
{
|
||||
case LogSeverity.Critical:
|
||||
case LogSeverity.Error:
|
||||
_logger.LogError(msg.Exception, msg.Message); break;
|
||||
case LogSeverity.Warning:
|
||||
_logger.LogWarning(msg.Exception, msg.Message); break;
|
||||
default:
|
||||
_logger.LogInformation(msg.Message); break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task RemoveUnregisteredUsers(CancellationToken token)
|
||||
{
|
||||
var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First();
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessUserRoles(guild, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _botServices.LogToChannel($"Error during user procesing: {ex.Message}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromDays(1)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessUserRoles(RestGuild guild, CancellationToken token)
|
||||
{
|
||||
using LightlessDbContext dbContext = await _dbContextFactory.CreateDbContextAsync(token).ConfigureAwait(false);
|
||||
var roleId = _configurationService.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleRegistered), 0);
|
||||
var kickUnregistered = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.KickNonRegisteredUsers), false);
|
||||
if (roleId == null) return;
|
||||
|
||||
var registrationRole = guild.Roles.FirstOrDefault(f => f.Id == roleId.Value);
|
||||
var registeredUsers = new HashSet<ulong>(await dbContext.LodeStoneAuth.AsNoTracking().Select(c => c.DiscordId).ToListAsync().ConfigureAwait(false));
|
||||
|
||||
var executionStartTime = DateTimeOffset.UtcNow;
|
||||
|
||||
int processedUsers = 0;
|
||||
int addedRoles = 0;
|
||||
int kickedUsers = 0;
|
||||
int totalRoles = 0;
|
||||
int toRemoveUsers = 0;
|
||||
int freshUsers = 0;
|
||||
|
||||
await _botServices.LogToChannel($"Starting to process registered users: Adding Role {registrationRole.Name}. Kick Stale Unregistered: {kickUnregistered}.").ConfigureAwait(false);
|
||||
|
||||
await foreach (var userList in guild.GetUsersAsync(new RequestOptions { CancelToken = token }).ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Processing chunk of {count} users, total processed: {proc}, total roles: {total}, roles added: {added}, users kicked: {kicked}, users plan to kick: {planToKick}, fresh user: {fresh}",
|
||||
userList.Count, processedUsers, totalRoles + addedRoles, addedRoles, kickedUsers, toRemoveUsers, freshUsers);
|
||||
foreach (var user in userList)
|
||||
{
|
||||
if (user.IsBot) continue;
|
||||
|
||||
if (registeredUsers.Contains(user.Id))
|
||||
{
|
||||
bool roleAdded = await _botServices.AddRegisteredRoleAsync(user, registrationRole).ConfigureAwait(false);
|
||||
if (roleAdded) addedRoles++;
|
||||
else totalRoles++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((executionStartTime - user.JoinedAt.Value).TotalDays > 7)
|
||||
{
|
||||
if (kickUnregistered)
|
||||
{
|
||||
await _botServices.KickUserAsync(user).ConfigureAwait(false);
|
||||
kickedUsers++;
|
||||
}
|
||||
else
|
||||
{
|
||||
toRemoveUsers++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
freshUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
processedUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
await _botServices.LogToChannel($"Processing registered users finished. Processed {processedUsers} users, added {addedRoles} roles and kicked {kickedUsers} users").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RemoveUsersNotInVanityRole(CancellationToken token)
|
||||
{
|
||||
var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First();
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Cleaning up Vanity UIDs");
|
||||
await _botServices.LogToChannel("Cleaning up Vanity UIDs").ConfigureAwait(false);
|
||||
_logger.LogInformation("Getting rest guild {guildName}", guild.Name);
|
||||
var restGuild = await _discordClient.Rest.GetGuildAsync(guild.Id).ConfigureAwait(false);
|
||||
|
||||
Dictionary<ulong, string> allowedRoleIds = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.VanityRoles), new Dictionary<ulong, string>());
|
||||
_logger.LogInformation($"Allowed role ids: {string.Join(", ", allowedRoleIds)}");
|
||||
|
||||
if (allowedRoleIds.Any())
|
||||
{
|
||||
using var db = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
|
||||
var aliasedUsers = await db.LodeStoneAuth.Include("User")
|
||||
.Where(c => c.User != null && !string.IsNullOrEmpty(c.User.Alias)).ToListAsync().ConfigureAwait(false);
|
||||
var aliasedGroups = await db.Groups.Include(u => u.Owner)
|
||||
.Where(c => !string.IsNullOrEmpty(c.Alias)).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var lodestoneAuth in aliasedUsers)
|
||||
{
|
||||
await CheckVanityForUser(restGuild, allowedRoleIds, db, lodestoneAuth, token).ConfigureAwait(false);
|
||||
|
||||
await Task.Delay(1000, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var group in aliasedGroups)
|
||||
{
|
||||
await CheckVanityForGroup(restGuild, allowedRoleIds, db, group, token).ConfigureAwait(false);
|
||||
|
||||
await Task.Delay(1000, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("No roles for command defined, no cleanup performed");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Something failed during checking vanity user uids");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Vanity UID cleanup complete");
|
||||
await Task.Delay(TimeSpan.FromHours(12), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckVanityForGroup(RestGuild restGuild, Dictionary<ulong, string> allowedRoleIds, LightlessDbContext db, Group group, CancellationToken token)
|
||||
{
|
||||
var groupPrimaryUser = group.OwnerUID;
|
||||
var groupOwner = await db.Auth.Include(u => u.User).SingleOrDefaultAsync(u => u.UserUID == group.OwnerUID).ConfigureAwait(false);
|
||||
if (groupOwner != null && !string.IsNullOrEmpty(groupOwner.PrimaryUserUID))
|
||||
{
|
||||
groupPrimaryUser = groupOwner.PrimaryUserUID;
|
||||
}
|
||||
|
||||
var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(f => f.User.UID == groupPrimaryUser).ConfigureAwait(false);
|
||||
RestGuildUser discordUser = null;
|
||||
if (lodestoneUser != null)
|
||||
{
|
||||
discordUser = await restGuild.GetUserAsync(lodestoneUser.DiscordId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Checking Group: {group.GID} [{group.Alias}], owned by {group.OwnerUID} ({groupPrimaryUser}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List<ulong>())}");
|
||||
|
||||
if (lodestoneUser == null || discordUser == null || !discordUser.RoleIds.Any(allowedRoleIds.Keys.Contains))
|
||||
{
|
||||
await _botServices.LogToChannel($"VANITY GID REMOVAL: <@{lodestoneUser?.DiscordId ?? 0}> ({lodestoneUser?.User?.UID}) - GID: {group.GID}, Vanity: {group.Alias}").ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation($"User {lodestoneUser?.User?.UID ?? "unknown"} not in allowed roles, deleting group alias for {group.GID}");
|
||||
group.Alias = null;
|
||||
db.Update(group);
|
||||
await db.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckVanityForUser(RestGuild restGuild, Dictionary<ulong, string> allowedRoleIds, LightlessDbContext db, LodeStoneAuth lodestoneAuth, CancellationToken token)
|
||||
{
|
||||
var discordUser = await restGuild.GetUserAsync(lodestoneAuth.DiscordId).ConfigureAwait(false);
|
||||
_logger.LogInformation($"Checking User: {lodestoneAuth.DiscordId}, {lodestoneAuth.User.UID} ({lodestoneAuth.User.Alias}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List<ulong>())}");
|
||||
|
||||
if (discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Keys.Contains(u)))
|
||||
{
|
||||
_logger.LogInformation($"User {lodestoneAuth.User.UID} not in allowed roles, deleting alias");
|
||||
await _botServices.LogToChannel($"VANITY UID REMOVAL: <@{lodestoneAuth.DiscordId}> - UID: {lodestoneAuth.User.UID}, Vanity: {lodestoneAuth.User.Alias}").ConfigureAwait(false);
|
||||
lodestoneAuth.User.Alias = null;
|
||||
var secondaryUsers = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var secondaryUser in secondaryUsers)
|
||||
{
|
||||
_logger.LogInformation($"Secondary User {secondaryUser.User.UID} not in allowed roles, deleting alias");
|
||||
|
||||
secondaryUser.User.Alias = null;
|
||||
db.Update(secondaryUser.User);
|
||||
}
|
||||
db.Update(lodestoneAuth.User);
|
||||
await db.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateStatusAsync(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var endPoint = _connectionMultiplexer.GetEndPoints().First();
|
||||
var onlineUsers = await _connectionMultiplexer.GetServer(endPoint).KeysAsync(pattern: "UID:*").CountAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Users online: " + onlineUsers);
|
||||
await _discordClient.SetActivityAsync(new Game("Lightless for " + onlineUsers + " Users")).ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public class DiscordBotServices
|
||||
{
|
||||
public readonly string[] LodestoneServers = ["eu", "na", "jp", "fr", "de"];
|
||||
public ConcurrentDictionary<ulong, string> DiscordLodestoneMapping = new();
|
||||
public ConcurrentDictionary<ulong, string> DiscordRelinkLodestoneMapping = new();
|
||||
public ConcurrentDictionary<ulong, bool> DiscordVerifiedUsers { get; } = new();
|
||||
public ConcurrentDictionary<ulong, DateTime> LastVanityChange = new();
|
||||
public ConcurrentDictionary<string, DateTime> LastVanityGidChange = new(StringComparer.Ordinal);
|
||||
public ConcurrentDictionary<ulong, ulong> ValidInteractions { get; } = new();
|
||||
public ConcurrentDictionary<RestRole, string> VanityRoles { get; set; } = new();
|
||||
public ConcurrentBag<ulong> VerifiedCaptchaUsers { get; } = new();
|
||||
private readonly IConfigurationService<ServicesConfiguration> _configuration;
|
||||
private readonly CancellationTokenSource verificationTaskCts = new();
|
||||
private RestGuild? _guild;
|
||||
private ulong? _logChannelId;
|
||||
private RestTextChannel? _logChannel;
|
||||
|
||||
public DiscordBotServices(ILogger<DiscordBotServices> logger, LightlessMetrics metrics,
|
||||
IConfigurationService<ServicesConfiguration> configuration)
|
||||
{
|
||||
Logger = logger;
|
||||
Metrics = metrics;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public ILogger<DiscordBotServices> Logger { get; init; }
|
||||
public LightlessMetrics Metrics { get; init; }
|
||||
public ConcurrentQueue<KeyValuePair<ulong, Func<DiscordBotServices, Task>>> VerificationQueue { get; } = new();
|
||||
|
||||
public Task Start()
|
||||
{
|
||||
_ = ProcessVerificationQueue();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Stop()
|
||||
{
|
||||
verificationTaskCts.Cancel();
|
||||
verificationTaskCts.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task LogToChannel(string msg)
|
||||
{
|
||||
if (_guild == null) return;
|
||||
Logger.LogInformation("LogToChannel: {msg}", msg);
|
||||
var logChannelId = _configuration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordChannelForBotLog), null);
|
||||
if (logChannelId == null) return;
|
||||
if (logChannelId != _logChannelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logChannelId = logChannelId;
|
||||
_logChannel = await _guild.GetTextChannelAsync(logChannelId.Value).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not get bot log channel");
|
||||
}
|
||||
}
|
||||
|
||||
if (_logChannel == null) return;
|
||||
await _logChannel.SendMessageAsync(msg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RetryAsync(Task action, IUser user, string operation, bool logInfoToChannel = true)
|
||||
{
|
||||
int retryCount = 0;
|
||||
int maxRetries = 5;
|
||||
var retryDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
while (retryCount < maxRetries)
|
||||
{
|
||||
try
|
||||
{
|
||||
await action.ConfigureAwait(false);
|
||||
if (logInfoToChannel)
|
||||
await LogToChannel($"{user.Mention} {operation} SUCCESS").ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
catch (RateLimitedException)
|
||||
{
|
||||
retryCount++;
|
||||
await LogToChannel($"{user.Mention} {operation} RATELIMIT, retry {retryCount} in {retryDelay}.").ConfigureAwait(false);
|
||||
await Task.Delay(retryDelay).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await LogToChannel($"{user.Mention} {operation} FAILED: {ex.Message}").ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (retryCount == maxRetries)
|
||||
{
|
||||
await LogToChannel($"{user.Mention} FAILED: RetryCount exceeded.").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveRegisteredRoleAsync(IUser user)
|
||||
{
|
||||
var registeredRole = _configuration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleRegistered), null);
|
||||
if (registeredRole == null) return;
|
||||
var restUser = await _guild.GetUserAsync(user.Id).ConfigureAwait(false);
|
||||
if (restUser == null) return;
|
||||
if (!restUser.RoleIds.Contains(registeredRole.Value)) return;
|
||||
await RetryAsync(restUser.RemoveRoleAsync(registeredRole.Value), user, $"Remove Registered Role").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task AddRegisteredRoleAsync(IUser user)
|
||||
{
|
||||
var registeredRole = _configuration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleRegistered), null);
|
||||
if (registeredRole == null) return;
|
||||
var restUser = await _guild.GetUserAsync(user.Id).ConfigureAwait(false);
|
||||
if (restUser == null) return;
|
||||
if (restUser.RoleIds.Contains(registeredRole.Value)) return;
|
||||
await RetryAsync(restUser.AddRoleAsync(registeredRole.Value), user, $"Add Registered Role").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> AddRegisteredRoleAsync(RestGuildUser user, RestRole role)
|
||||
{
|
||||
if (user.RoleIds.Contains(role.Id)) return false;
|
||||
await RetryAsync(user.AddRoleAsync(role), user, $"Add Registered Role", false).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task KickUserAsync(RestGuildUser user)
|
||||
{
|
||||
await RetryAsync(user.KickAsync("No registration found"), user, "Kick").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ProcessVerificationQueue()
|
||||
{
|
||||
while (!verificationTaskCts.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogDebug("Processing Verification Queue, Entries: {entr}", VerificationQueue.Count);
|
||||
if (VerificationQueue.TryPeek(out var queueitem))
|
||||
{
|
||||
try
|
||||
{
|
||||
await queueitem.Value.Invoke(this).ConfigureAwait(false);
|
||||
Logger.LogInformation("Processed Verification for {key}", queueitem.Key);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "Error during queue work");
|
||||
}
|
||||
finally
|
||||
{
|
||||
VerificationQueue.TryDequeue(out _);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateGuild(RestGuild guild)
|
||||
{
|
||||
_guild = guild;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
// todo: remove all this crap at some point
|
||||
|
||||
public class LodestoneModal : IModal
|
||||
{
|
||||
public string Title => "Verify with Lodestone";
|
||||
|
||||
[InputLabel("Enter the Lodestone URL of your Character")]
|
||||
[ModalTextInput("lodestone_url", TextInputStyle.Short, "https://*.finalfantasyxiv.com/lodestone/character/<CHARACTERID>/")]
|
||||
public string LodestoneUrl { get; set; }
|
||||
}
|
||||
293
LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs
Normal file
293
LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Services;
|
||||
using StackExchange.Redis;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public class LightlessModule : InteractionModuleBase
|
||||
{
|
||||
private readonly ILogger<LightlessModule> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IConfigurationService<ServicesConfiguration> _lightlessServicesConfiguration;
|
||||
private readonly IConnectionMultiplexer _connectionMultiplexer;
|
||||
|
||||
public LightlessModule(ILogger<LightlessModule> logger, IServiceProvider services,
|
||||
IConfigurationService<ServicesConfiguration> lightlessServicesConfiguration,
|
||||
IConnectionMultiplexer connectionMultiplexer)
|
||||
{
|
||||
_logger = logger;
|
||||
_services = services;
|
||||
_lightlessServicesConfiguration = lightlessServicesConfiguration;
|
||||
_connectionMultiplexer = connectionMultiplexer;
|
||||
}
|
||||
|
||||
[SlashCommand("userinfo", "Shows you your user information")]
|
||||
public async Task UserInfo([Summary("secondary_uid", "(Optional) Your secondary UID")] string? secondaryUid = null,
|
||||
[Summary("discord_user", "ADMIN ONLY: Discord User to check for")] IUser? discordUser = null,
|
||||
[Summary("uid", "ADMIN ONLY: UID to check for")] string? uid = null)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}",
|
||||
Context.Interaction.User.Id, nameof(UserInfo));
|
||||
|
||||
try
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
|
||||
eb = await HandleUserInfo(eb, Context.User.Id, secondaryUid, discordUser?.Id ?? null, uid);
|
||||
|
||||
await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("An error occured");
|
||||
eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine);
|
||||
|
||||
await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[SlashCommand("useradd", "ADMIN ONLY: add a user unconditionally to the Database")]
|
||||
public async Task UserAdd([Summary("desired_uid", "Desired UID")] string desiredUid)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{params}",
|
||||
Context.Interaction.User.Id, nameof(UserAdd),
|
||||
string.Join(",", new[] { $"{nameof(desiredUid)}:{desiredUid}" }));
|
||||
|
||||
try
|
||||
{
|
||||
var embed = await HandleUserAdd(desiredUid, Context.User.Id);
|
||||
|
||||
await RespondAsync(embeds: new[] { embed }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("An error occured");
|
||||
eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine);
|
||||
|
||||
await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[SlashCommand("message", "ADMIN ONLY: sends a message to clients")]
|
||||
public async Task SendMessageToClients([Summary("message", "Message to send")] string message,
|
||||
[Summary("severity", "Severity of the message")] MessageSeverity messageType = MessageSeverity.Information,
|
||||
[Summary("uid", "User ID to the person to send the message to")] string? uid = null)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{message}:{type}:{uid}", Context.Interaction.User.Id, nameof(SendMessageToClients), message, messageType, uid);
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
using var db = scope.ServiceProvider.GetService<LightlessDbContext>();
|
||||
|
||||
if (!(await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == Context.Interaction.User.Id))?.User?.IsAdmin ?? true)
|
||||
{
|
||||
await RespondAsync("No permission", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(uid) && !await db.Users.AnyAsync(u => u.UID == uid))
|
||||
{
|
||||
await RespondAsync("Specified UID does not exist", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using HttpClient c = new HttpClient();
|
||||
await c.PostAsJsonAsync(new Uri(_lightlessServicesConfiguration.GetValue<Uri>
|
||||
(nameof(ServicesConfiguration.MainServerAddress)), "/msgc/sendMessage"), new ClientMessage(messageType, message, uid ?? string.Empty))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var discordChannelForMessages = _lightlessServicesConfiguration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordChannelForMessages), null);
|
||||
if (uid == null && discordChannelForMessages != null)
|
||||
{
|
||||
var discordChannel = await Context.Guild.GetChannelAsync(discordChannelForMessages.Value) as IMessageChannel;
|
||||
if (discordChannel != null)
|
||||
{
|
||||
var embedColor = messageType switch
|
||||
{
|
||||
MessageSeverity.Information => Color.Blue,
|
||||
MessageSeverity.Warning => new Color(255, 255, 0),
|
||||
MessageSeverity.Error => Color.Red,
|
||||
_ => Color.Blue
|
||||
};
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle(messageType + " server message");
|
||||
eb.WithColor(embedColor);
|
||||
eb.WithDescription(message);
|
||||
|
||||
await discordChannel.SendMessageAsync(embed: eb.Build());
|
||||
}
|
||||
}
|
||||
|
||||
await RespondAsync("Message sent", ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await RespondAsync("Failed to send message: " + ex.ToString(), ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Embed> HandleUserAdd(string desiredUid, ulong discordUserId)
|
||||
{
|
||||
var embed = new EmbedBuilder();
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
using var db = scope.ServiceProvider.GetService<LightlessDbContext>();
|
||||
if (!(await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == discordUserId))?.User?.IsAdmin ?? true)
|
||||
{
|
||||
embed.WithTitle("Failed to add user");
|
||||
embed.WithDescription("No permission");
|
||||
}
|
||||
else if (db.Users.Any(u => u.UID == desiredUid || u.Alias == desiredUid))
|
||||
{
|
||||
embed.WithTitle("Failed to add user");
|
||||
embed.WithDescription("Already in Database");
|
||||
}
|
||||
else
|
||||
{
|
||||
User newUser = new()
|
||||
{
|
||||
IsAdmin = false,
|
||||
IsModerator = false,
|
||||
LastLoggedIn = DateTime.UtcNow,
|
||||
UID = desiredUid,
|
||||
};
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = StringUtils.Sha256String(computedHash),
|
||||
User = newUser,
|
||||
};
|
||||
|
||||
await db.Users.AddAsync(newUser);
|
||||
await db.Auth.AddAsync(auth);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
embed.WithTitle("Successfully added " + desiredUid);
|
||||
embed.WithDescription("Secret Key: " + computedHash);
|
||||
}
|
||||
|
||||
return embed.Build();
|
||||
}
|
||||
|
||||
private async Task<EmbedBuilder> HandleUserInfo(EmbedBuilder eb, ulong id, string? secondaryUserUid = null, ulong? optionalUser = null, string? uid = null)
|
||||
{
|
||||
bool showForSecondaryUser = secondaryUserUid != null;
|
||||
using var scope = _services.CreateScope();
|
||||
await using var db = scope.ServiceProvider.GetRequiredService<LightlessDbContext>();
|
||||
|
||||
var primaryUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false);
|
||||
|
||||
ulong userToCheckForDiscordId = id;
|
||||
|
||||
if (primaryUser == null)
|
||||
{
|
||||
eb.WithTitle("No account");
|
||||
eb.WithDescription("No Lightless account was found associated to your Discord user");
|
||||
return eb;
|
||||
}
|
||||
|
||||
bool isAdminCall = primaryUser.User.IsModerator || primaryUser.User.IsAdmin;
|
||||
|
||||
if ((optionalUser != null || uid != null) && !isAdminCall)
|
||||
{
|
||||
eb.WithTitle("Unauthorized");
|
||||
eb.WithDescription("You are not authorized to view another users' information");
|
||||
return eb;
|
||||
}
|
||||
else if ((optionalUser != null || uid != null) && isAdminCall)
|
||||
{
|
||||
LodeStoneAuth userInDb = null;
|
||||
if (optionalUser != null)
|
||||
{
|
||||
userInDb = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == optionalUser).ConfigureAwait(false);
|
||||
}
|
||||
else if (uid != null)
|
||||
{
|
||||
userInDb = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid || u.User.Alias == uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (userInDb == null)
|
||||
{
|
||||
eb.WithTitle("No account");
|
||||
eb.WithDescription("The Discord user has no valid Lightless account");
|
||||
return eb;
|
||||
}
|
||||
|
||||
userToCheckForDiscordId = userInDb.DiscordId;
|
||||
}
|
||||
|
||||
var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == userToCheckForDiscordId).ConfigureAwait(false);
|
||||
var dbUser = lodestoneUser.User;
|
||||
if (showForSecondaryUser)
|
||||
{
|
||||
dbUser = (await db.Auth.Include(u => u.User).SingleOrDefaultAsync(u => u.PrimaryUserUID == dbUser.UID && u.UserUID == secondaryUserUid))?.User;
|
||||
if (dbUser == null)
|
||||
{
|
||||
eb.WithTitle("No such secondary UID");
|
||||
eb.WithDescription($"A secondary UID {secondaryUserUid} was not found attached to your primary UID {primaryUser.User.UID}.");
|
||||
return eb;
|
||||
}
|
||||
}
|
||||
|
||||
var auth = await db.Auth.Include(u => u.PrimaryUser).SingleOrDefaultAsync(u => u.UserUID == dbUser.UID).ConfigureAwait(false);
|
||||
var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var identity = await _connectionMultiplexer.GetDatabase().StringGetAsync("UID:" + dbUser.UID).ConfigureAwait(false);
|
||||
|
||||
eb.WithTitle("User Information");
|
||||
eb.WithDescription("This is the user information for Discord User <@" + userToCheckForDiscordId + ">" + Environment.NewLine + Environment.NewLine
|
||||
+ "If you want to verify your secret key is valid, go to https://emn178.github.io/online-tools/sha256.html and copy your secret key into there and compare it to the Hashed Secret Key provided below.");
|
||||
eb.AddField("UID", dbUser.UID);
|
||||
if (!string.IsNullOrEmpty(dbUser.Alias))
|
||||
{
|
||||
eb.AddField("Vanity UID", dbUser.Alias);
|
||||
}
|
||||
if (showForSecondaryUser)
|
||||
{
|
||||
eb.AddField("Primary UID for " + dbUser.UID, auth.PrimaryUserUID);
|
||||
}
|
||||
else
|
||||
{
|
||||
var secondaryUIDs = await db.Auth.Where(p => p.PrimaryUserUID == dbUser.UID).Select(p => p.UserUID).ToListAsync();
|
||||
if (secondaryUIDs.Any())
|
||||
{
|
||||
eb.AddField("Secondary UIDs", string.Join(Environment.NewLine, secondaryUIDs));
|
||||
}
|
||||
}
|
||||
eb.AddField("Last Online (UTC)", dbUser.LastLoggedIn.ToString("U"));
|
||||
eb.AddField("Currently online ", !string.IsNullOrEmpty(identity));
|
||||
eb.AddField("Hashed Secret Key", auth.HashedKey);
|
||||
eb.AddField("Joined Syncshells", groupsJoined.Count);
|
||||
eb.AddField("Owned Syncshells", groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var syncShellUserCount = await db.GroupPairs.CountAsync(g => g.GroupGID == group.GID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(group.Alias))
|
||||
{
|
||||
eb.AddField("Owned Syncshell " + group.GID + " Vanity ID", group.Alias);
|
||||
}
|
||||
eb.AddField("Owned Syncshell " + group.GID + " User Count", syncShellUserCount);
|
||||
}
|
||||
|
||||
if (isAdminCall && !string.IsNullOrEmpty(identity))
|
||||
{
|
||||
eb.AddField("Character Ident", identity);
|
||||
}
|
||||
|
||||
return eb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule : InteractionModuleBase
|
||||
{
|
||||
private const int _totalAprilFoolsRoles = 200;
|
||||
private const string _persistentFileName = "april2024.json";
|
||||
|
||||
private static readonly SemaphoreSlim _fileSemaphore = new(1, 1);
|
||||
|
||||
[ComponentInteraction("wizard-fools")]
|
||||
public async Task ComponentFools()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentFools), Context.Interaction.User.Id);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("WorryCoin™ and LightlessToken© Balance");
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithDescription("You currently have" + Environment.NewLine + Environment.NewLine
|
||||
+ "**200000** MaTE©" + Environment.NewLine
|
||||
+ "**0** WorryCoin™" + Environment.NewLine + Environment.NewLine
|
||||
+ "You have no payment method set up. Press the button below to add a payment method.");
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
cb.WithButton("Add Payment Method", "wizard-fools-start", ButtonStyle.Primary, emote: new Emoji("💲"));
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-fools-start")]
|
||||
public async Task ComponentFoolsStart()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentFoolsStart), Context.Interaction.User.Id);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false);
|
||||
bool userIsInPermanentVanityRole = _botServices.VanityRoles.Where(v => !v.Value.Contains('$', StringComparison.Ordinal))
|
||||
.Select(v => v.Key).Any(u => user.RoleIds.Contains(u.Id)) || !_botServices.VanityRoles.Any();
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
|
||||
var participatedUsers = await GetParticipants().ConfigureAwait(false);
|
||||
var remainingRoles = _totalAprilFoolsRoles - participatedUsers.Count(c => c.Value == true);
|
||||
|
||||
if (userIsInPermanentVanityRole)
|
||||
{
|
||||
eb.WithColor(Color.Green);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "__As you already have a role that gives you a permanent Vanity ID, you cannot win another one here. "
|
||||
+ "However, tell your friends as this bot will give them a chance to win one of " + _totalAprilFoolsRoles + " lifetime vanity roles.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The giveaway is active until <t:" + (new DateTime(2024, 04, 01, 23, 59, 59, DateTimeKind.Utc).Subtract(DateTime.UnixEpoch).TotalSeconds) + ":f>.");
|
||||
}
|
||||
else if (participatedUsers.ContainsKey(Context.User.Id))
|
||||
{
|
||||
eb.WithColor(Color.Orange);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "__You already participated in the giveaway of the permanent Vanity roles and therefore cannot participate again. Better luck next time!__");
|
||||
}
|
||||
else if (remainingRoles > 0)
|
||||
{
|
||||
eb.WithColor(Color.Green);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "You have currently no permanent role that allows you to set a Vanity ID, however I am giving away a total of " + _totalAprilFoolsRoles + " permanent vanity roles "
|
||||
+ "(" + remainingRoles + " still remain) and you can win one using this bot!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "To win you simply have to pick one of the buttons labeled \"Win\" below this post. Which button will win is random. "
|
||||
+ "There is a 1 in 5 chance that you can win the role. __You can only participate once.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The giveaway is active until <t:" + (new DateTime(2024, 04, 01, 23, 59, 59, DateTimeKind.Utc).Subtract(DateTime.UnixEpoch).TotalSeconds) + ":f>.");
|
||||
cb.WithButton("Win", "wizard-fools-win:1", ButtonStyle.Primary, new Emoji("1️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:2", ButtonStyle.Primary, new Emoji("2️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:3", ButtonStyle.Primary, new Emoji("3️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:4", ButtonStyle.Primary, new Emoji("4️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:5", ButtonStyle.Primary, new Emoji("5️⃣"));
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Orange);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "__I have been giving away " + _totalAprilFoolsRoles + " permanent Vanity ID roles for this server, however you are sadly too late as they ran out by now. "
|
||||
+ "Better luck next year with whatever I will come up with!__");
|
||||
}
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-fools-win:*")]
|
||||
public async Task ComponentFoolsWin(int number)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentFoolsWin), Context.Interaction.User.Id);
|
||||
|
||||
var winningNumber = new Random().Next(1, 6);
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
bool hasWon = winningNumber == number;
|
||||
|
||||
await WriteParticipants(Context.Interaction.User.Id, hasWon).ConfigureAwait(false);
|
||||
|
||||
if (hasWon)
|
||||
{
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithTitle("Congratulations you are winner!");
|
||||
eb.WithDescription("You, by pure accident and sheer luck, picked the right number and have won yourself a lifetime Vanity ID role on this server!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The role will remain as long as you remain on this server, if you happen to leave it you will not get the role back."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Head over to Home and to the Vanity IDs section to set it up for your account!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Once again, thank you for participating and have a great day.");
|
||||
|
||||
var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false);
|
||||
await user.AddRoleAsync(_lightlessServicesConfiguration.GetValue<ulong?>(nameof(ServicesConfiguration.DiscordRoleAprilFools2024)).Value).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Fortune did not bless you");
|
||||
eb.WithDescription("You, through sheer misfortune, sadly did not pick the right number. (The winning number was " + winningNumber + ")"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Better luck next time!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Once again, thank you for participating and regardless, have a great day.");
|
||||
}
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<ulong, bool>> GetParticipants()
|
||||
{
|
||||
await _fileSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_persistentFileName))
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_persistentFileName).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize<Dictionary<ulong, bool>>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteParticipants(ulong participant, bool win)
|
||||
{
|
||||
await _fileSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
Dictionary<ulong, bool> participants = new();
|
||||
if (File.Exists(_persistentFileName))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(_persistentFileName).ConfigureAwait(false);
|
||||
participants = JsonSerializer.Deserialize<Dictionary<ulong, bool>>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// probably empty file just deal with it
|
||||
}
|
||||
}
|
||||
|
||||
participants[participant] = win;
|
||||
|
||||
await File.WriteAllTextAsync(_persistentFileName, JsonSerializer.Serialize(participants)).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-delete")]
|
||||
public async Task ComponentDelete()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentDelete), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Delete Account");
|
||||
eb.WithDescription("You can delete your primary or secondary UIDs here." + Environment.NewLine + Environment.NewLine
|
||||
+ "__Note: deleting your primary UID will delete all associated secondary UIDs as well.__" + Environment.NewLine + Environment.NewLine
|
||||
+ "- 1️⃣ is your primary account/UID" + Environment.NewLine
|
||||
+ "- 2️⃣ are all your secondary accounts/UIDs" + Environment.NewLine
|
||||
+ "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection.");
|
||||
eb.WithColor(Color.Blue);
|
||||
|
||||
ComponentBuilder cb = new();
|
||||
await AddUserSelection(lightlessDb, cb, "wizard-delete-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-delete-select")]
|
||||
public async Task SelectionDeleteAccount(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionDeleteAccount), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
bool isPrimary = lightlessDb.Auth.Single(u => u.UserUID == uid).PrimaryUserUID == null;
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle($"Are you sure you want to delete {uid}?");
|
||||
eb.WithDescription($"This operation is irreversible. All your pairs, joined syncshells and information stored on the service for {uid} will be " +
|
||||
$"irrevocably deleted." +
|
||||
(isPrimary ? (Environment.NewLine + Environment.NewLine +
|
||||
"⚠️ **You are about to delete a Primary UID, all attached Secondary UIDs and their information will be deleted as well.** ⚠️") : string.Empty));
|
||||
eb.WithColor(Color.Purple);
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-delete", emote: new Emoji("❌"));
|
||||
cb.WithButton($"Delete {uid}", "wizard-delete-confirm:" + uid, ButtonStyle.Danger, emote: new Emoji("🗑️"));
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-delete-confirm:*")]
|
||||
public async Task ComponentDeleteAccountConfirm(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(ComponentDeleteAccountConfirm), Context.Interaction.User.Id, uid);
|
||||
|
||||
await RespondWithModalAsync<ConfirmDeletionModal>("wizard-delete-confirm-modal:" + uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-delete-confirm-modal:*")]
|
||||
public async Task ModalDeleteAccountConfirm(string uid, ConfirmDeletionModal modal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(ModalDeleteAccountConfirm), Context.Interaction.User.Id, uid);
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.Equals("DELETE", modal.Delete, StringComparison.Ordinal))
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Did not confirm properly");
|
||||
eb.WithDescription($"You entered {modal.Delete} but requested was DELETE. Please try again and enter DELETE to confirm.");
|
||||
eb.WithColor(Color.Red);
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-delete", emote: new Emoji("❌"));
|
||||
cb.WithButton("Retry", "wizard-delete-confirm:" + uid, emote: new Emoji("🔁"));
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var maxGroupsByUser = _lightlessClientConfigurationService.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var user = await db.Users.SingleAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
var lodestone = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid).ConfigureAwait(false);
|
||||
await SharedDbFunctions.PurgeUser(_logger, user, db, maxGroupsByUser).ConfigureAwait(false);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle($"Account {uid} successfully deleted");
|
||||
eb.WithColor(Color.Green);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} DELETE SUCCESS: {uid}").ConfigureAwait(false);
|
||||
|
||||
// only remove role if deleted uid has lodestone attached (== primary uid)
|
||||
if (lodestone != null)
|
||||
{
|
||||
await _botServices.RemoveRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling modal delete account confirm");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-recover")]
|
||||
public async Task ComponentRecover()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentRecover), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Blue);
|
||||
eb.WithTitle("Recover");
|
||||
eb.WithDescription("In case you have lost your secret key you can recover it here." + Environment.NewLine + Environment.NewLine
|
||||
+ "## ⚠️ **Once you recover your key, the previously used key will be invalidated. If you use Lightless on multiple devices you will have to update the key everywhere you use it.** ⚠️" + Environment.NewLine + Environment.NewLine
|
||||
+ "Use the selection below to select the user account you want to recover." + Environment.NewLine + Environment.NewLine
|
||||
+ "- 1️⃣ is your primary account/UID" + Environment.NewLine
|
||||
+ "- 2️⃣ are all your secondary accounts/UIDs" + Environment.NewLine
|
||||
+ "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection." + Environment.NewLine
|
||||
+ "# Note: instead of recovery and handling secret keys the switch to OAuth2 authentication is strongly suggested.");
|
||||
ComponentBuilder cb = new();
|
||||
await AddUserSelection(lightlessDb, cb, "wizard-recover-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-recover-select")]
|
||||
public async Task SelectionRecovery(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionRecovery), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Green);
|
||||
await HandleRecovery(lightlessDb, eb, uid).ConfigureAwait(false);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleRecovery(LightlessDbContext db, EmbedBuilder embed, string uid)
|
||||
{
|
||||
string computedHash = string.Empty;
|
||||
Auth auth;
|
||||
var previousAuth = await db.Auth.Include(u => u.User).FirstOrDefaultAsync(u => u.UserUID == uid).ConfigureAwait(false);
|
||||
if (previousAuth != null)
|
||||
{
|
||||
db.Auth.Remove(previousAuth);
|
||||
}
|
||||
|
||||
computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
string hashedKey = StringUtils.Sha256String(computedHash);
|
||||
auth = new Auth()
|
||||
{
|
||||
HashedKey = hashedKey,
|
||||
User = previousAuth.User,
|
||||
PrimaryUserUID = previousAuth.PrimaryUserUID
|
||||
};
|
||||
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
|
||||
embed.WithTitle($"Recovery for {uid} complete");
|
||||
embed.WithDescription("This is your new private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "**__NOTE: Secret keys are considered legacy authentication. If you are using the suggested OAuth2 authentication, you do not need to use the Secret Key or recover ever again.__**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"||**`{computedHash}`**||"
|
||||
+ Environment.NewLine
|
||||
+ "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Enter this key in the Lightless Sync Service Settings and reconnect to the service.");
|
||||
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_botServices.Logger.LogInformation("User recovered: {userUID}:{hashedKey}", previousAuth.UserUID, hashedKey);
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} RECOVER SUCCESS: {previousAuth.UserUID}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-register")]
|
||||
public async Task ComponentRegister()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentRegister), Context.Interaction.User.Id);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Blue);
|
||||
eb.WithTitle("Start Registration");
|
||||
eb.WithDescription("Here you can start the registration process with the Lightless Sync server of this Discord." + Environment.NewLine + Environment.NewLine
|
||||
+ "- Have your Lodestone URL ready (i.e. https://eu.finalfantasyxiv.com/lodestone/character/XXXXXXXXX)" + Environment.NewLine
|
||||
+ " - The registration requires you to modify your Lodestone profile with a generated code for verification" + Environment.NewLine
|
||||
+ "- Do not use this on mobile because you will need to be able to copy the generated secret key" + Environment.NewLine
|
||||
+ "# Follow the bot instructions precisely. Slow down and read.");
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
cb.WithButton("Start Registration", "wizard-register-start", ButtonStyle.Primary, emote: new Emoji("🌒"));
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-register-start")]
|
||||
public async Task ComponentRegisterStart()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentRegisterStart), Context.Interaction.User.Id);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var entry = await db.LodeStoneAuth.SingleOrDefaultAsync(u => u.DiscordId == Context.User.Id && u.StartedAt != null).ConfigureAwait(false);
|
||||
if (entry != null)
|
||||
{
|
||||
db.LodeStoneAuth.Remove(entry);
|
||||
}
|
||||
_botServices.DiscordLodestoneMapping.TryRemove(Context.User.Id, out _);
|
||||
_botServices.DiscordVerifiedUsers.TryRemove(Context.User.Id, out _);
|
||||
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await RespondWithModalAsync<LodestoneModal>("wizard-register-lodestone-modal").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-register-lodestone-modal")]
|
||||
public async Task ModalRegister(LodestoneModal lodestoneModal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{lodestone}", nameof(ModalRegister), Context.Interaction.User.Id, lodestoneModal.LodestoneUrl);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
var success = await HandleRegisterModalAsync(eb, lodestoneModal).ConfigureAwait(false);
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
if (success.Item1) cb.WithButton("Verify", "wizard-register-verify:" + success.Item2, ButtonStyle.Primary, emote: new Emoji("✅"));
|
||||
else cb.WithButton("Try again", "wizard-register-start", ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-register-verify:*")]
|
||||
public async Task ComponentRegisterVerify(string verificationCode)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{verificationcode}", nameof(ComponentRegisterVerify), Context.Interaction.User.Id, verificationCode);
|
||||
|
||||
_botServices.VerificationQueue.Enqueue(new KeyValuePair<ulong, Func<DiscordBotServices, Task>>(Context.User.Id,
|
||||
(service) => HandleVerifyAsync(Context.User.Id, verificationCode, service)));
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Check", "wizard-register-verify-check:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("❓"));
|
||||
eb.WithTitle("Verification Pending");
|
||||
eb.WithDescription("Please wait until the bot verifies your registration." + Environment.NewLine
|
||||
+ "Press \"Check\" to check if the verification has been already processed" + Environment.NewLine + Environment.NewLine
|
||||
+ "__This will not advance automatically, you need to press \"Check\".__");
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-register-verify-check:*")]
|
||||
public async Task ComponentRegisterVerifyCheck(string verificationCode)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(ComponentRegisterVerifyCheck), Context.Interaction.User.Id, verificationCode);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
bool stillEnqueued = _botServices.VerificationQueue.Any(k => k.Key == Context.User.Id);
|
||||
bool verificationRan = _botServices.DiscordVerifiedUsers.TryGetValue(Context.User.Id, out bool verified);
|
||||
bool registerSuccess = false;
|
||||
if (!verificationRan)
|
||||
{
|
||||
if (stillEnqueued)
|
||||
{
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithTitle("Your verification is still pending");
|
||||
eb.WithDescription("Please try again and click Check in a few seconds");
|
||||
cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Check", "wizard-register-verify-check:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("❓"));
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Something went wrong");
|
||||
eb.WithDescription("Your verification was processed but did not arrive properly. Please try to start the registration from the start.");
|
||||
cb.WithButton("Restart", "wizard-register", ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (verified)
|
||||
{
|
||||
eb.WithColor(Color.Green);
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var (uid, key) = await HandleAddUser(db).ConfigureAwait(false);
|
||||
eb.WithTitle($"Registration successful, your UID: {uid}");
|
||||
eb.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "**__NOTE: Secret keys are considered legacy. Using the suggested OAuth2 authentication in Lightless, you do not need to use this Secret Key.__**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"||**`{key}`**||"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "If you want to continue using legacy authentication, enter this key in Lightless Sync and hit save to connect to the service."
|
||||
+ Environment.NewLine
|
||||
+ "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__"
|
||||
+ Environment.NewLine
|
||||
+ "You should connect as soon as possible to not get caught by the automatic cleanup process."
|
||||
+ Environment.NewLine
|
||||
+ "Have fun.");
|
||||
AddHome(cb);
|
||||
registerSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithTitle("Failed to verify registration");
|
||||
eb.WithDescription("The bot was not able to find the required verification code on your Lodestone profile."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Please restart your verification process, make sure to save your profile _twice_ for it to be properly saved."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "If this link does not lead to your profile edit page, you __need__ to configure the privacy settings first: https://na.finalfantasyxiv.com/lodestone/my/setting/profile/"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "**Make sure your profile is set to public (All Users) for your character. The bot cannot read profiles with privacy settings set to \"logged in\" or \"private\".**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "## You __need__ to enter following the code this bot provided onto your Lodestone in the character profile:"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "**`" + verificationCode + "`**");
|
||||
cb.WithButton("Cancel", "wizard-register", emote: new Emoji("❌"));
|
||||
cb.WithButton("Retry", "wizard-register-verify:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
}
|
||||
}
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
if (registerSuccess)
|
||||
await _botServices.AddRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool, string)> HandleRegisterModalAsync(EmbedBuilder embed, LodestoneModal arg)
|
||||
{
|
||||
var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.LodestoneUrl);
|
||||
if (lodestoneId == null)
|
||||
{
|
||||
embed.WithTitle("Invalid Lodestone URL");
|
||||
embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine
|
||||
+ "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/");
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
// check if userid is already in db
|
||||
var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString());
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
|
||||
// check if discord id or lodestone id is banned
|
||||
if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == hashedLodestoneId))
|
||||
{
|
||||
embed.WithDescription("This account is banned");
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
if (db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId))
|
||||
{
|
||||
// character already in db
|
||||
embed.WithDescription("This lodestone character already exists in the Database. If you want to attach this character to your current Discord account use relink.");
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
string lodestoneAuth = await GenerateLodestoneAuth(Context.User.Id, hashedLodestoneId, db).ConfigureAwait(false);
|
||||
// check if lodestone id is already in db
|
||||
embed.WithTitle("Authorize your character");
|
||||
embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/"
|
||||
+ Environment.NewLine
|
||||
+ "__NOTE: If the link does not lead you to your character edit profile page, you need to log in and set up your privacy settings!__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"**`{lodestoneAuth}`**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN LIGHTLESS !**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Once added and saved, use the button below to Verify and finish registration and receive a secret key to use for Lightless Sync."
|
||||
+ Environment.NewLine
|
||||
+ "__You can delete the entry from your profile after verification.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The verification will expire in approximately 15 minutes. If you fail to verify the registration will be invalidated and you have to register again.");
|
||||
_botServices.DiscordLodestoneMapping[Context.User.Id] = lodestoneId.ToString();
|
||||
|
||||
return (true, lodestoneAuth);
|
||||
}
|
||||
|
||||
private async Task HandleVerifyAsync(ulong userid, string authString, DiscordBotServices services)
|
||||
{
|
||||
using var req = new HttpClient();
|
||||
|
||||
services.DiscordVerifiedUsers.Remove(userid, out _);
|
||||
if (services.DiscordLodestoneMapping.ContainsKey(userid))
|
||||
{
|
||||
var randomServer = services.LodestoneServers[random.Next(services.LodestoneServers.Length)];
|
||||
var url = $"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{services.DiscordLodestoneMapping[userid]}";
|
||||
using var response = await req.GetAsync(url).ConfigureAwait(false);
|
||||
_logger.LogInformation("Verifying {userid} with URL {url}", userid, url);
|
||||
if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
if (content.Contains(authString))
|
||||
{
|
||||
services.DiscordVerifiedUsers[userid] = true;
|
||||
_logger.LogInformation("Verified {userid} from lodestone {lodestone}", userid, services.DiscordLodestoneMapping[userid]);
|
||||
await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Success.").ConfigureAwait(false);
|
||||
services.DiscordLodestoneMapping.TryRemove(userid, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.DiscordVerifiedUsers[userid] = false;
|
||||
_logger.LogInformation("Could not verify {userid} from lodestone {lodestone}, did not find authString: {authString}, status code was: {code}",
|
||||
userid, services.DiscordLodestoneMapping[userid], authString, response.StatusCode);
|
||||
await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Failed: No Authstring ({authString}). (<{url}>)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Could not verify {userid}, HttpStatusCode: {code}", userid, response.StatusCode);
|
||||
await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Failed: HttpStatusCode {response.StatusCode}. (<{url}>)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string, string)> HandleAddUser(LightlessDbContext db)
|
||||
{
|
||||
var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == Context.User.Id);
|
||||
|
||||
var user = new User();
|
||||
|
||||
var hasValidUid = false;
|
||||
while (!hasValidUid)
|
||||
{
|
||||
var uid = StringUtils.GenerateRandomString(10);
|
||||
if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue;
|
||||
user.UID = uid;
|
||||
hasValidUid = true;
|
||||
}
|
||||
|
||||
// make the first registered user on the service to admin
|
||||
if (!await db.Users.AnyAsync().ConfigureAwait(false))
|
||||
{
|
||||
user.IsAdmin = true;
|
||||
}
|
||||
|
||||
user.LastLoggedIn = DateTime.UtcNow;
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
string hashedKey = StringUtils.Sha256String(computedHash);
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = hashedKey,
|
||||
User = user,
|
||||
};
|
||||
|
||||
await db.Users.AddAsync(user).ConfigureAwait(false);
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
|
||||
lodestoneAuth.StartedAt = null;
|
||||
lodestoneAuth.User = user;
|
||||
lodestoneAuth.LodestoneAuthString = null;
|
||||
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_botServices.Logger.LogInformation("User registered: {userUID}:{hashedKey}", user.UID, hashedKey);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} REGISTER COMPLETE: => {user.UID}").ConfigureAwait(false);
|
||||
|
||||
_botServices.DiscordVerifiedUsers.Remove(Context.User.Id, out _);
|
||||
|
||||
return (user.UID, computedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-relink")]
|
||||
public async Task ComponentRelink()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentRelink), Context.Interaction.User.Id);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Relink");
|
||||
eb.WithColor(Color.Blue);
|
||||
eb.WithDescription("Use this in case you already have a registered Lightless account, but lost access to your previous Discord account." + Environment.NewLine + Environment.NewLine
|
||||
+ "- Have your original registered Lodestone URL ready (i.e. https://eu.finalfantasyxiv.com/lodestone/character/XXXXXXXXX)" + Environment.NewLine
|
||||
+ " - The relink process requires you to modify your Lodestone profile with a generated code for verification" + Environment.NewLine
|
||||
+ "- Do not use this on mobile because you will need to be able to copy the generated secret key");
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
cb.WithButton("Start Relink", "wizard-relink-start", ButtonStyle.Primary, emote: new Emoji("🔗"));
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-relink-start")]
|
||||
public async Task ComponentRelinkStart()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentRelinkStart), Context.Interaction.User.Id);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
db.LodeStoneAuth.RemoveRange(db.LodeStoneAuth.Where(u => u.DiscordId == Context.User.Id));
|
||||
_botServices.DiscordVerifiedUsers.TryRemove(Context.User.Id, out _);
|
||||
_botServices.DiscordRelinkLodestoneMapping.TryRemove(Context.User.Id, out _);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await RespondWithModalAsync<LodestoneModal>("wizard-relink-lodestone-modal").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-relink-lodestone-modal")]
|
||||
public async Task ModalRelink(LodestoneModal lodestoneModal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{url}", nameof(ModalRelink), Context.Interaction.User.Id, lodestoneModal.LodestoneUrl);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
var result = await HandleRelinkModalAsync(eb, lodestoneModal).ConfigureAwait(false);
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
if (result.Success) cb.WithButton("Verify", "wizard-relink-verify:" + result.LodestoneAuth + "," + result.UID, ButtonStyle.Primary, emote: new Emoji("✅"));
|
||||
else cb.WithButton("Try again", "wizard-relink-start", ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-relink-verify:*,*")]
|
||||
public async Task ComponentRelinkVerify(string verificationCode, string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}:{verificationCode}", nameof(ComponentRelinkVerify), Context.Interaction.User.Id, uid, verificationCode);
|
||||
|
||||
|
||||
_botServices.VerificationQueue.Enqueue(new KeyValuePair<ulong, Func<DiscordBotServices, Task>>(Context.User.Id,
|
||||
(services) => HandleVerifyRelinkAsync(Context.User.Id, verificationCode, services)));
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Check", "wizard-relink-verify-check:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("❓"));
|
||||
eb.WithTitle("Relink Verification Pending");
|
||||
eb.WithDescription("Please wait until the bot verifies your registration." + Environment.NewLine
|
||||
+ "Press \"Check\" to check if the verification has been already processed" + Environment.NewLine + Environment.NewLine
|
||||
+ "__This will not advance automatically, you need to press \"Check\".__");
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-relink-verify-check:*,*")]
|
||||
public async Task ComponentRelinkVerifyCheck(string verificationCode, string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}:{verificationCode}", nameof(ComponentRelinkVerifyCheck), Context.Interaction.User.Id, uid, verificationCode);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
bool stillEnqueued = _botServices.VerificationQueue.Any(k => k.Key == Context.User.Id);
|
||||
bool verificationRan = _botServices.DiscordVerifiedUsers.TryGetValue(Context.User.Id, out bool verified);
|
||||
bool relinkSuccess = false;
|
||||
if (!verificationRan)
|
||||
{
|
||||
if (stillEnqueued)
|
||||
{
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithTitle("Your relink verification is still pending");
|
||||
eb.WithDescription("Please try again and click Check in a few seconds");
|
||||
cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Check", "wizard-relink-verify-check:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("❓"));
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Something went wrong");
|
||||
eb.WithDescription("Your relink verification was processed but did not arrive properly. Please try to start the relink process from the start.");
|
||||
cb.WithButton("Restart", "wizard-relink", ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (verified)
|
||||
{
|
||||
eb.WithColor(Color.Green);
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var (_, key) = await HandleRelinkUser(db, uid).ConfigureAwait(false);
|
||||
eb.WithTitle($"Relink successful, your UID is again: {uid}");
|
||||
eb.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"||**`{key}`**||"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Enter this key in Lightless Sync and hit save to connect to the service."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "NOTE: If you are using OAuth2, you do not require to use this secret key."
|
||||
+ Environment.NewLine
|
||||
+ "Have fun.");
|
||||
AddHome(cb);
|
||||
|
||||
relinkSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithTitle("Failed to verify relink");
|
||||
eb.WithDescription("The bot was not able to find the required verification code on your Lodestone profile." + Environment.NewLine + Environment.NewLine
|
||||
+ "Please restart your relink process, make sure to save your profile _twice_ for it to be properly saved." + Environment.NewLine + Environment.NewLine
|
||||
+ "**Make sure your profile is set to public (All Users) for your character. The bot cannot read profiles with privacy settings set to \"logged in\" or \"private\".**" + Environment.NewLine + Environment.NewLine
|
||||
+ "The code the bot is looking for is" + Environment.NewLine + Environment.NewLine
|
||||
+ "**`" + verificationCode + "`**");
|
||||
cb.WithButton("Cancel", "wizard-relink", emote: new Emoji("❌"));
|
||||
cb.WithButton("Retry", "wizard-relink-verify:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
}
|
||||
}
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
if (relinkSuccess)
|
||||
await _botServices.AddRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string LodestoneAuth, string UID)> HandleRelinkModalAsync(EmbedBuilder embed, LodestoneModal arg)
|
||||
{
|
||||
ulong userId = Context.User.Id;
|
||||
|
||||
var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.LodestoneUrl);
|
||||
if (lodestoneId == null)
|
||||
{
|
||||
embed.WithTitle("Invalid Lodestone URL");
|
||||
embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine
|
||||
+ "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/");
|
||||
return (false, string.Empty, string.Empty);
|
||||
}
|
||||
// check if userid is already in db
|
||||
var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString());
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
|
||||
// check if discord id or lodestone id is banned
|
||||
if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == hashedLodestoneId))
|
||||
{
|
||||
embed.WithTitle("Illegal operation");
|
||||
embed.WithDescription("Your account is banned");
|
||||
return (false, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
if (!db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId))
|
||||
{
|
||||
// character already in db
|
||||
embed.WithTitle("Impossible operation");
|
||||
embed.WithDescription("This lodestone character does not exist in the database.");
|
||||
return (false, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
var expectedUser = await db.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.HashedLodestoneId == hashedLodestoneId).ConfigureAwait(false);
|
||||
|
||||
string lodestoneAuth = await GenerateLodestoneAuth(Context.User.Id, hashedLodestoneId, db).ConfigureAwait(false);
|
||||
// check if lodestone id is already in db
|
||||
embed.WithTitle("Authorize your character for relinking");
|
||||
embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"**`{lodestoneAuth}`**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN LIGHTLESS !**"
|
||||
+ Environment.NewLine
|
||||
+ "__You can delete the entry from your profile after verification.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The verification will expire in approximately 15 minutes. If you fail to verify the relink will be invalidated and you have to relink again.");
|
||||
_botServices.DiscordRelinkLodestoneMapping[Context.User.Id] = lodestoneId.ToString();
|
||||
|
||||
return (true, lodestoneAuth, expectedUser.User.UID);
|
||||
}
|
||||
|
||||
private async Task HandleVerifyRelinkAsync(ulong userid, string authString, DiscordBotServices services)
|
||||
{
|
||||
using var req = new HttpClient();
|
||||
|
||||
services.DiscordVerifiedUsers.Remove(userid, out _);
|
||||
if (services.DiscordRelinkLodestoneMapping.ContainsKey(userid))
|
||||
{
|
||||
var randomServer = services.LodestoneServers[random.Next(services.LodestoneServers.Length)];
|
||||
var url = $"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{services.DiscordRelinkLodestoneMapping[userid]}";
|
||||
_logger.LogInformation("Verifying {userid} with URL {url}", userid, url);
|
||||
using var response = await req.GetAsync(url).ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
if (content.Contains(authString))
|
||||
{
|
||||
services.DiscordVerifiedUsers[userid] = true;
|
||||
_logger.LogInformation("Relink: Verified {userid} from lodestone {lodestone}", userid, services.DiscordRelinkLodestoneMapping[userid]);
|
||||
await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Success.").ConfigureAwait(false);
|
||||
services.DiscordRelinkLodestoneMapping.TryRemove(userid, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.DiscordVerifiedUsers[userid] = false;
|
||||
_logger.LogInformation("Relink: Could not verify {userid} from lodestone {lodestone}, did not find authString: {authString}, status code was: {code}",
|
||||
userid, services.DiscordRelinkLodestoneMapping[userid], authString, response.StatusCode);
|
||||
await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Failed: No Authstring ({authString}). (<{url}>)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Could not verify {userid}, HttpStatusCode: {code}", userid, response.StatusCode);
|
||||
await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Failed: HttpStatusCode {response.StatusCode}. (<{url}>)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string, string)> HandleRelinkUser(LightlessDbContext db, string uid)
|
||||
{
|
||||
var oldLodestoneAuth = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid && u.DiscordId != Context.User.Id).ConfigureAwait(false);
|
||||
var newLodestoneAuth = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false);
|
||||
|
||||
var user = oldLodestoneAuth.User;
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = StringUtils.Sha256String(computedHash),
|
||||
User = user,
|
||||
};
|
||||
|
||||
var previousAuth = await db.Auth.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
|
||||
if (previousAuth != null)
|
||||
{
|
||||
db.Remove(previousAuth);
|
||||
}
|
||||
|
||||
newLodestoneAuth.LodestoneAuthString = null;
|
||||
newLodestoneAuth.StartedAt = null;
|
||||
newLodestoneAuth.User = user;
|
||||
db.Update(newLodestoneAuth);
|
||||
db.Remove(oldLodestoneAuth);
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
|
||||
_botServices.Logger.LogInformation("User relinked: {userUID}", user.UID);
|
||||
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} RELINK COMPLETE: => {user.UID}").ConfigureAwait(false);
|
||||
|
||||
return (user.UID, computedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-secondary")]
|
||||
public async Task ComponentSecondary()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentSecondary), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
var primaryUID = (await lightlessDb.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User.UID;
|
||||
var secondaryUids = await lightlessDb.Auth.CountAsync(p => p.PrimaryUserUID == primaryUID).ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Blue);
|
||||
eb.WithTitle("Secondary UID");
|
||||
eb.WithDescription("You can create secondary UIDs here. " + Environment.NewLine + Environment.NewLine
|
||||
+ "Secondary UIDs act as completely separate Lightless accounts with their own pair list, joined syncshells, UID and so on." + Environment.NewLine
|
||||
+ "Use this to create UIDs if you want to use Lightless on two separate game instances at once or keep your alts private." + Environment.NewLine + Environment.NewLine
|
||||
+ "__Note:__ Creating a Secondary UID is _not_ necessary to use Lightless for alts." + Environment.NewLine + Environment.NewLine
|
||||
+ $"You currently have {secondaryUids} Secondary UIDs out of a maximum of 20.");
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
cb.WithButton("Create Secondary UID", "wizard-secondary-create:" + primaryUID, ButtonStyle.Primary, emote: new Emoji("2️⃣"), disabled: secondaryUids >= 20);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-secondary-create:*")]
|
||||
public async Task ComponentSecondaryCreate(string primaryUid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{primary}", nameof(ComponentSecondaryCreate), Context.Interaction.User.Id, primaryUid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Secondary UID created");
|
||||
eb.WithColor(Color.Green);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
await HandleAddSecondary(lightlessDb, eb, primaryUid).ConfigureAwait(false);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task HandleAddSecondary(LightlessDbContext db, EmbedBuilder embed, string primaryUID)
|
||||
{
|
||||
User newUser = new()
|
||||
{
|
||||
IsAdmin = false,
|
||||
IsModerator = false,
|
||||
LastLoggedIn = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
var hasValidUid = false;
|
||||
while (!hasValidUid)
|
||||
{
|
||||
var uid = StringUtils.GenerateRandomString(10);
|
||||
if (await db.Users.AnyAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false)) continue;
|
||||
newUser.UID = uid;
|
||||
hasValidUid = true;
|
||||
}
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = StringUtils.Sha256String(computedHash),
|
||||
User = newUser,
|
||||
PrimaryUserUID = primaryUID
|
||||
};
|
||||
|
||||
await db.Users.AddAsync(newUser).ConfigureAwait(false);
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
embed.WithDescription("A secondary UID for you was created, use the information below and add the secret key to the Lightless setings in the Service Settings tab.");
|
||||
embed.AddField("UID", newUser.UID);
|
||||
embed.AddField("Secret Key", computedHash);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} SECONDARY SUCCESS: {newUser.UID}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-userinfo")]
|
||||
public async Task ComponentUserinfo()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentUserinfo), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("User Info");
|
||||
eb.WithColor(Color.Blue);
|
||||
eb.WithDescription("You can see information about your user account(s) here." + Environment.NewLine
|
||||
+ "Use the selection below to select a user account to see info for." + Environment.NewLine + Environment.NewLine
|
||||
+ "- 1️⃣ is your primary account/UID" + Environment.NewLine
|
||||
+ "- 2️⃣ are all your secondary accounts/UIDs" + Environment.NewLine
|
||||
+ "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection.");
|
||||
ComponentBuilder cb = new();
|
||||
await AddUserSelection(lightlessDb, cb, "wizard-userinfo-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-userinfo-select")]
|
||||
public async Task SelectionUserinfo(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionUserinfo), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle($"User Info for {uid}");
|
||||
await HandleUserInfo(eb, lightlessDb, uid).ConfigureAwait(false);
|
||||
eb.WithColor(Color.Green);
|
||||
ComponentBuilder cb = new();
|
||||
await AddUserSelection(lightlessDb, cb, "wizard-userinfo-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleUserInfo(EmbedBuilder eb, LightlessDbContext db, string uid)
|
||||
{
|
||||
ulong userToCheckForDiscordId = Context.User.Id;
|
||||
|
||||
var dbUser = await db.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
|
||||
var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var identity = await _connectionMultiplexer.GetDatabase().StringGetAsync("UID:" + dbUser.UID).ConfigureAwait(false);
|
||||
|
||||
eb.WithDescription("This is the user info for your selected UID. You can check other UIDs or go back using the menu below.");
|
||||
if (!string.IsNullOrEmpty(dbUser.Alias))
|
||||
{
|
||||
eb.AddField("Vanity UID", dbUser.Alias);
|
||||
}
|
||||
eb.AddField("Last Online (UTC)", dbUser.LastLoggedIn.ToString("U"));
|
||||
eb.AddField("Currently online ", !string.IsNullOrEmpty(identity));
|
||||
eb.AddField("Joined Syncshells", groupsJoined.Count);
|
||||
eb.AddField("Owned Syncshells", groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var syncShellUserCount = await db.GroupPairs.CountAsync(g => g.GroupGID == group.GID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(group.Alias))
|
||||
{
|
||||
eb.AddField("Owned Syncshell " + group.GID + " Vanity ID", group.Alias);
|
||||
}
|
||||
eb.AddField("Owned Syncshell " + group.GID + " User Count", syncShellUserCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-vanity")]
|
||||
public async Task ComponentVanity()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentVanity), Context.Interaction.User.Id);
|
||||
|
||||
StringBuilder sb = new();
|
||||
var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false);
|
||||
bool userIsInVanityRole = _botServices.VanityRoles.Keys.Any(u => user.RoleIds.Contains(u.Id)) || !_botServices.VanityRoles.Any();
|
||||
if (!userIsInVanityRole)
|
||||
{
|
||||
sb.AppendLine("To be able to set Vanity IDs you must have one of the following roles:");
|
||||
foreach (var role in _botServices.VanityRoles)
|
||||
{
|
||||
sb.Append("- ").Append(role.Key.Mention).Append(" (").Append(role.Value).AppendLine(")");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("Your current roles on this server allow you to set Vanity IDs.");
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Vanity IDs");
|
||||
eb.WithDescription("You are able to set your Vanity IDs here." + Environment.NewLine
|
||||
+ "Vanity IDs are a way to customize your displayed UID or Syncshell ID to others." + Environment.NewLine + Environment.NewLine
|
||||
+ sb.ToString());
|
||||
eb.WithColor(Color.Blue);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
if (userIsInVanityRole)
|
||||
{
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
await AddUserSelection(db, cb, "wizard-vanity-uid").ConfigureAwait(false);
|
||||
await AddGroupSelection(db, cb, "wizard-vanity-gid").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-uid")]
|
||||
public async Task SelectionVanityUid(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityUid), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var user = db.Users.Single(u => u.UID == uid);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
eb.WithTitle($"Set Vanity UID for {uid}");
|
||||
eb.WithDescription($"You are about to change the Vanity UID for {uid}" + Environment.NewLine + Environment.NewLine
|
||||
+ "The current Vanity UID is set to: **" + (user.Alias == null ? "No Vanity UID set" : user.Alias) + "**");
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Set Vanity ID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-uid-set:*")]
|
||||
public async Task SelectionVanityUidSet(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityUidSet), Context.Interaction.User.Id, uid);
|
||||
|
||||
await RespondWithModalAsync<VanityUidModal>("wizard-vanity-uid-modal:" + uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-vanity-uid-modal:*")]
|
||||
public async Task ConfirmVanityUidModal(string uid, VanityUidModal modal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}:{vanity}", nameof(ConfirmVanityUidModal), Context.Interaction.User.Id, uid, modal.DesiredVanityUID);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
var desiredVanityUid = modal.DesiredVanityUID;
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
bool canAddVanityId = !db.Users.Any(u => u.UID == modal.DesiredVanityUID || u.Alias == modal.DesiredVanityUID);
|
||||
|
||||
Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,15}$", RegexOptions.ECMAScript);
|
||||
if (!rgx.Match(desiredVanityUid).Success)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Invalid Vanity UID");
|
||||
eb.WithDescription("A Vanity UID must be between 5 and 15 characters long and only contain the letters A-Z, numbers 0-9, dashes (-) and underscores (_).");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different UID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else if (!canAddVanityId)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Vanity UID already taken");
|
||||
eb.WithDescription($"The Vanity UID {desiredVanityUid} has already been claimed. Please pick a different one.");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different UID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await db.Users.SingleAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
user.Alias = desiredVanityUid;
|
||||
db.Update(user);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
eb.WithColor(Color.Green);
|
||||
eb.WithTitle("Vanity UID successfully set");
|
||||
eb.WithDescription($"Your Vanity UID for \"{uid}\" was successfully changed to \"{desiredVanityUid}\"." + Environment.NewLine + Environment.NewLine
|
||||
+ "For changes to take effect you need to reconnect to the Lightless service.");
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} VANITY UID SET: UID: {user.UID}, Vanity: {desiredVanityUid}").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
}
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-gid")]
|
||||
public async Task SelectionVanityGid(string gid)
|
||||
{
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityGid), Context.Interaction.User.Id, gid);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var group = db.Groups.Single(u => u.GID == gid);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
eb.WithTitle($"Set Vanity GID for {gid}");
|
||||
eb.WithDescription($"You are about to change the Vanity Syncshell ID for {gid}" + Environment.NewLine + Environment.NewLine
|
||||
+ "The current Vanity Syncshell ID is set to: **" + (group.Alias == null ? "No Vanity Syncshell ID set" : group.Alias) + "**");
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Set Vanity ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-gid-set:*")]
|
||||
public async Task SelectionVanityGidSet(string gid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{gid}", nameof(SelectionVanityGidSet), Context.Interaction.User.Id, gid);
|
||||
|
||||
await RespondWithModalAsync<VanityGidModal>("wizard-vanity-gid-modal:" + gid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-vanity-gid-modal:*")]
|
||||
public async Task ConfirmVanityGidModal(string gid, VanityGidModal modal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{gid}:{vanity}", nameof(ConfirmVanityGidModal), Context.Interaction.User.Id, gid, modal.DesiredVanityGID);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
var desiredVanityGid = modal.DesiredVanityGID;
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
bool canAddVanityId = !db.Groups.Any(u => u.GID == modal.DesiredVanityGID || u.Alias == modal.DesiredVanityGID);
|
||||
|
||||
Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,20}$", RegexOptions.ECMAScript);
|
||||
if (!rgx.Match(desiredVanityGid).Success)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Invalid Vanity Syncshell ID");
|
||||
eb.WithDescription("A Vanity Syncshell ID must be between 5 and 20 characters long and only contain the letters A-Z, numbers 0-9, dashes (-) and underscores (_).");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else if (!canAddVanityId)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Vanity Syncshell ID already taken");
|
||||
eb.WithDescription($"The Vanity Synshell ID \"{desiredVanityGid}\" has already been claimed. Please pick a different one.");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var group = await db.Groups.SingleAsync(u => u.GID == gid).ConfigureAwait(false);
|
||||
group.Alias = desiredVanityGid;
|
||||
db.Update(group);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
eb.WithColor(Color.Green);
|
||||
eb.WithTitle("Vanity Syncshell ID successfully set");
|
||||
eb.WithDescription($"Your Vanity Syncshell ID for {gid} was successfully changed to \"{desiredVanityGid}\"." + Environment.NewLine + Environment.NewLine
|
||||
+ "For changes to take effect you need to reconnect to the Lightless service.");
|
||||
AddHome(cb);
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} VANITY GID SET: GID: {group.GID}, Vanity: {desiredVanityGid}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using Discord.WebSocket;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule : InteractionModuleBase
|
||||
{
|
||||
private ILogger<LightlessModule> _logger;
|
||||
private DiscordBotServices _botServices;
|
||||
private IConfigurationService<ServerConfiguration> _lightlessClientConfigurationService;
|
||||
private IConfigurationService<ServicesConfiguration> _lightlessServicesConfiguration;
|
||||
private IConnectionMultiplexer _connectionMultiplexer;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
|
||||
private Random random = new();
|
||||
|
||||
public LightlessWizardModule(ILogger<LightlessModule> logger, DiscordBotServices botServices,
|
||||
IConfigurationService<ServerConfiguration> lightlessClientConfigurationService,
|
||||
IConfigurationService<ServicesConfiguration> lightlessServicesConfiguration,
|
||||
IConnectionMultiplexer connectionMultiplexer, IDbContextFactory<LightlessDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_botServices = botServices;
|
||||
_lightlessClientConfigurationService = lightlessClientConfigurationService;
|
||||
_lightlessServicesConfiguration = lightlessServicesConfiguration;
|
||||
_connectionMultiplexer = connectionMultiplexer;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-captcha:*")]
|
||||
public async Task WizardCaptcha(bool init = false)
|
||||
{
|
||||
if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
if (_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id))
|
||||
{
|
||||
await StartWizard(true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
|
||||
Random rnd = new Random();
|
||||
var correctButton = rnd.Next(4) + 1;
|
||||
string nthButtonText = correctButton switch
|
||||
{
|
||||
1 => "first",
|
||||
2 => "second",
|
||||
3 => "third",
|
||||
4 => "fourth",
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
Emoji nthButtonEmoji = correctButton switch
|
||||
{
|
||||
1 => new Emoji("⬅️"),
|
||||
2 => new Emoji("🤖"),
|
||||
3 => new Emoji("‼️"),
|
||||
4 => new Emoji("✉️"),
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
eb.WithTitle("Lightless Bot Services Captcha");
|
||||
eb.WithDescription("You are seeing this embed because you interact with this bot for the first time since the bot has been restarted." + Environment.NewLine + Environment.NewLine
|
||||
+ "This bot __requires__ embeds for its function. To proceed, please verify you have embeds enabled." + Environment.NewLine
|
||||
+ $"## To verify you have embeds enabled __press on the **{nthButtonText}** button ({nthButtonEmoji}).__");
|
||||
eb.WithColor(Color.LightOrange);
|
||||
|
||||
int incorrectButtonHighlight = 1;
|
||||
do
|
||||
{
|
||||
incorrectButtonHighlight = rnd.Next(4) + 1;
|
||||
}
|
||||
while (incorrectButtonHighlight == correctButton);
|
||||
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("This", correctButton == 1 ? "wizard-home:false" : "wizard-captcha-fail:1", emote: new Emoji("⬅️"), style: incorrectButtonHighlight == 1 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
cb.WithButton("Bot", correctButton == 2 ? "wizard-home:false" : "wizard-captcha-fail:2", emote: new Emoji("🤖"), style: incorrectButtonHighlight == 2 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
cb.WithButton("Requires", correctButton == 3 ? "wizard-home:false" : "wizard-captcha-fail:3", emote: new Emoji("‼️"), style: incorrectButtonHighlight == 3 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
cb.WithButton("Embeds", correctButton == 4 ? "wizard-home:false" : "wizard-captcha-fail:4", emote: new Emoji("✉️"), style: incorrectButtonHighlight == 4 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
|
||||
await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task InitOrUpdateInteraction(bool init, EmbedBuilder eb, ComponentBuilder cb)
|
||||
{
|
||||
if (init)
|
||||
{
|
||||
await RespondAsync(embed: eb.Build(), components: cb.Build(), ephemeral: true).ConfigureAwait(false);
|
||||
var resp = await GetOriginalResponseAsync().ConfigureAwait(false);
|
||||
_botServices.ValidInteractions[Context.User.Id] = resp.Id;
|
||||
_logger.LogInformation("Init Msg: {id}", resp.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-captcha-fail:*")]
|
||||
public async Task WizardCaptchaFail(int button)
|
||||
{
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Restart (with Embeds enabled)", "wizard-captcha:false", emote: new Emoji("↩️"));
|
||||
await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m =>
|
||||
{
|
||||
m.Embed = null;
|
||||
m.Content = "You pressed the wrong button. You likely have embeds disabled. Enable embeds in your Discord client (Settings -> Chat -> \"Show embeds and preview website links pasted into chat\") and try again.";
|
||||
m.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} FAILED CAPTCHA").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
[ComponentInteraction("wizard-home:*")]
|
||||
public async Task StartWizard(bool init = false)
|
||||
{
|
||||
if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
if (!_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id))
|
||||
_botServices.VerifiedCaptchaUsers.Add(Context.Interaction.User.Id);
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(StartWizard), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
bool hasAccount = await lightlessDb.LodeStoneAuth.AnyAsync(u => u.DiscordId == Context.User.Id && u.StartedAt == null).ConfigureAwait(false);
|
||||
|
||||
if (init)
|
||||
{
|
||||
bool isBanned = await lightlessDb.BannedRegistrations.AnyAsync(u => u.DiscordIdOrLodestoneAuth == Context.User.Id.ToString()).ConfigureAwait(false);
|
||||
|
||||
if (isBanned)
|
||||
{
|
||||
EmbedBuilder ebBanned = new();
|
||||
ebBanned.WithTitle("You are not welcome here");
|
||||
ebBanned.WithDescription("Your Discord account is banned");
|
||||
await RespondAsync(embed: ebBanned.Build(), ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#if !DEBUG
|
||||
bool isInAprilFoolsMode = _lightlessServicesConfiguration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleAprilFools2024), null) != null
|
||||
&& DateTime.UtcNow.Month == 4 && DateTime.UtcNow.Day == 1 && DateTime.UtcNow.Year == 2024 && DateTime.UtcNow.Hour >= 10;
|
||||
#elif DEBUG
|
||||
bool isInAprilFoolsMode = true;
|
||||
#endif
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Welcome to the Lightless Sync Service Bot for this server");
|
||||
eb.WithDescription("Here is what you can do:" + Environment.NewLine + Environment.NewLine
|
||||
+ (!hasAccount ? string.Empty : ("- Check your account status press \"ℹ️ User Info\"" + Environment.NewLine))
|
||||
+ (hasAccount ? string.Empty : ("- Register a new Lightless Account press \"🌒 Register\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : ("- You lost your secret key press \"🏥 Recover\"" + Environment.NewLine))
|
||||
+ (hasAccount ? string.Empty : ("- If you have changed your Discord account press \"🔗 Relink\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : ("- Create a secondary UIDs press \"2️⃣ Secondary UID\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : ("- Set a Vanity UID press \"💅 Vanity IDs\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : (!isInAprilFoolsMode ? string.Empty : ("- Check your WorryCoin™ and LightlessToken© balance and add payment options" + Environment.NewLine)))
|
||||
+ (!hasAccount ? string.Empty : ("- Delete your primary or secondary accounts with \"⚠️ Delete\""))
|
||||
);
|
||||
eb.WithColor(Color.Blue);
|
||||
ComponentBuilder cb = new();
|
||||
if (!hasAccount)
|
||||
{
|
||||
cb.WithButton("Register", "wizard-register", ButtonStyle.Primary, new Emoji("🌒"));
|
||||
cb.WithButton("Relink", "wizard-relink", ButtonStyle.Secondary, new Emoji("🔗"));
|
||||
}
|
||||
else
|
||||
{
|
||||
cb.WithButton("User Info", "wizard-userinfo", ButtonStyle.Secondary, new Emoji("ℹ️"));
|
||||
cb.WithButton("Recover", "wizard-recover", ButtonStyle.Secondary, new Emoji("🏥"));
|
||||
cb.WithButton("Secondary UID", "wizard-secondary", ButtonStyle.Secondary, new Emoji("2️⃣"));
|
||||
cb.WithButton("Vanity IDs", "wizard-vanity", ButtonStyle.Secondary, new Emoji("💅"));
|
||||
if (isInAprilFoolsMode)
|
||||
{
|
||||
cb.WithButton("WorryCoin™ and LightlessToken© management", "wizard-fools", ButtonStyle.Primary, new Emoji("💲"));
|
||||
}
|
||||
cb.WithButton("Delete", "wizard-delete", ButtonStyle.Danger, new Emoji("⚠️"));
|
||||
}
|
||||
|
||||
await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public class VanityUidModal : IModal
|
||||
{
|
||||
public string Title => "Set Vanity UID";
|
||||
|
||||
[InputLabel("Set your Vanity UID")]
|
||||
[ModalTextInput("vanity_uid", TextInputStyle.Short, "5-15 characters, underscore, dash", 5, 15)]
|
||||
public string DesiredVanityUID { get; set; }
|
||||
}
|
||||
|
||||
public class VanityGidModal : IModal
|
||||
{
|
||||
public string Title => "Set Vanity Syncshell ID";
|
||||
|
||||
[InputLabel("Set your Vanity Syncshell ID")]
|
||||
[ModalTextInput("vanity_gid", TextInputStyle.Short, "5-20 characters, underscore, dash", 5, 20)]
|
||||
public string DesiredVanityGID { get; set; }
|
||||
}
|
||||
|
||||
public class ConfirmDeletionModal : IModal
|
||||
{
|
||||
public string Title => "Confirm Account Deletion";
|
||||
|
||||
[InputLabel("Enter \"DELETE\" in all Caps")]
|
||||
[ModalTextInput("confirmation", TextInputStyle.Short, "Enter DELETE")]
|
||||
public string Delete { get; set; }
|
||||
}
|
||||
|
||||
private async Task<LightlessDbContext> GetDbContext()
|
||||
{
|
||||
return await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateInteraction()
|
||||
{
|
||||
if (Context.Interaction is not IComponentInteraction componentInteraction) return true;
|
||||
|
||||
if (_botServices.ValidInteractions.TryGetValue(Context.User.Id, out ulong interactionId) && interactionId == componentInteraction.Message.Id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Session expired");
|
||||
eb.WithDescription("This session has expired since you have either again pressed \"Start\" on the initial message or the bot has been restarted." + Environment.NewLine + Environment.NewLine
|
||||
+ "Please use the newly started interaction or start a new one.");
|
||||
eb.WithColor(Color.Red);
|
||||
ComponentBuilder cb = new();
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddHome(ComponentBuilder cb)
|
||||
{
|
||||
cb.WithButton("Return to Home", "wizard-home:false", ButtonStyle.Secondary, new Emoji("🏠"));
|
||||
}
|
||||
|
||||
private async Task ModifyModalInteraction(EmbedBuilder eb, ComponentBuilder cb)
|
||||
{
|
||||
await (Context.Interaction as SocketModal).UpdateAsync(m =>
|
||||
{
|
||||
m.Embed = eb.Build();
|
||||
m.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ModifyInteraction(EmbedBuilder eb, ComponentBuilder cb)
|
||||
{
|
||||
await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m =>
|
||||
{
|
||||
m.Content = null;
|
||||
m.Embed = eb.Build();
|
||||
m.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task AddUserSelection(LightlessDbContext lightlessDb, ComponentBuilder cb, string customId)
|
||||
{
|
||||
var discordId = Context.User.Id;
|
||||
var existingAuth = await lightlessDb.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(e => e.DiscordId == discordId).ConfigureAwait(false);
|
||||
if (existingAuth != null)
|
||||
{
|
||||
SelectMenuBuilder sb = new();
|
||||
sb.WithPlaceholder("Select a UID");
|
||||
sb.WithCustomId(customId);
|
||||
var existingUids = await lightlessDb.Auth.Include(u => u.User).Where(u => u.UserUID == existingAuth.User.UID || u.PrimaryUserUID == existingAuth.User.UID)
|
||||
.OrderByDescending(u => u.PrimaryUser == null).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var entry in existingUids)
|
||||
{
|
||||
sb.AddOption(string.IsNullOrEmpty(entry.User.Alias) ? entry.UserUID : entry.User.Alias,
|
||||
entry.UserUID,
|
||||
!string.IsNullOrEmpty(entry.User.Alias) ? entry.User.UID : null,
|
||||
entry.PrimaryUserUID == null ? new Emoji("1️⃣") : new Emoji("2️⃣"));
|
||||
}
|
||||
cb.WithSelectMenu(sb);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddGroupSelection(LightlessDbContext db, ComponentBuilder cb, string customId)
|
||||
{
|
||||
var primary = (await db.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User;
|
||||
var secondary = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == primary.UID).Select(u => u.User).ToListAsync().ConfigureAwait(false);
|
||||
var primaryGids = (await db.Groups.Include(u => u.Owner).Where(u => u.OwnerUID == primary.UID).ToListAsync().ConfigureAwait(false));
|
||||
var secondaryGids = (await db.Groups.Include(u => u.Owner).Where(u => secondary.Select(u => u.UID).Contains(u.OwnerUID)).ToListAsync().ConfigureAwait(false));
|
||||
SelectMenuBuilder gids = new();
|
||||
if (primaryGids.Any() || secondaryGids.Any())
|
||||
{
|
||||
foreach (var item in primaryGids)
|
||||
{
|
||||
gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("1️⃣"));
|
||||
}
|
||||
foreach (var item in secondaryGids)
|
||||
{
|
||||
gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("2️⃣"));
|
||||
}
|
||||
gids.WithCustomId(customId);
|
||||
gids.WithPlaceholder("Select a Syncshell");
|
||||
cb.WithSelectMenu(gids);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, LightlessDbContext dbContext)
|
||||
{
|
||||
var auth = StringUtils.GenerateRandomString(12, "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz");
|
||||
LodeStoneAuth lsAuth = new LodeStoneAuth()
|
||||
{
|
||||
DiscordId = discordid,
|
||||
HashedLodestoneId = hashedLodestoneId,
|
||||
LodestoneAuthString = auth,
|
||||
StartedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.Add(lsAuth);
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return (auth);
|
||||
}
|
||||
|
||||
private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl)
|
||||
{
|
||||
var regex = new Regex(@"https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/\d+");
|
||||
var matches = regex.Match(lodestoneUrl);
|
||||
var isLodestoneUrl = matches.Success;
|
||||
if (!isLodestoneUrl || matches.Groups.Count < 1) return null;
|
||||
|
||||
lodestoneUrl = matches.Groups[0].ToString();
|
||||
var stringId = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last();
|
||||
if (!int.TryParse(stringId, out int lodestoneId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return lodestoneId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="appsettings.Development.json" />
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.Development.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.17.0" />
|
||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.184">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||
<ProjectReference Include="..\LightlessSyncShared\LightlessSyncShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
57
LightlessSyncServer/LightlessSyncServices/Program.cs
Normal file
57
LightlessSyncServer/LightlessSyncServices/Program.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using LightlessSyncServices;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var hostBuilder = CreateHostBuilder(args);
|
||||
var host = hostBuilder.Build();
|
||||
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var options = host.Services.GetService<IConfigurationService<ServicesConfiguration>>();
|
||||
var optionsServer = host.Services.GetService<IConfigurationService<ServerConfiguration>>();
|
||||
var logger = host.Services.GetService<ILogger<Program>>();
|
||||
logger.LogInformation("Loaded LightlessSync Services Configuration (IsMain: {isMain})", options.IsMain);
|
||||
logger.LogInformation(options.ToString());
|
||||
logger.LogInformation("Loaded LightlessSync Server Configuration (IsMain: {isMain})", optionsServer.IsMain);
|
||||
logger.LogInformation(optionsServer.ToString());
|
||||
}
|
||||
|
||||
host.Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureAppConfiguration((ctx, config) =>
|
||||
{
|
||||
var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH");
|
||||
if (!string.IsNullOrEmpty(appSettingsPath))
|
||||
{
|
||||
config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
||||
}
|
||||
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseContentRoot(AppContext.BaseDirectory);
|
||||
webBuilder.ConfigureLogging((ctx, builder) =>
|
||||
{
|
||||
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
|
||||
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
|
||||
});
|
||||
webBuilder.ConfigureKestrel((opt) =>
|
||||
{
|
||||
});
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"profiles": {
|
||||
"LightlessSyncServices": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5294;https://localhost:7294",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
LightlessSyncServer/LightlessSyncServices/Startup.cs
Normal file
76
LightlessSyncServer/LightlessSyncServices/Startup.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using LightlessSyncServices.Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Services;
|
||||
using StackExchange.Redis;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
namespace LightlessSyncServices;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<LightlessConfigurationBase>>();
|
||||
|
||||
var metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(LightlessConfigurationBase.MetricsPort), 4982));
|
||||
metricServer.Start();
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
var lightlessConfig = Configuration.GetSection("LightlessSync");
|
||||
|
||||
services.AddDbContextPool<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, Configuration.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024));
|
||||
services.AddDbContextFactory<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("LightlessSyncShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
});
|
||||
|
||||
services.AddSingleton(m => new LightlessMetrics(m.GetService<ILogger<LightlessMetrics>>(), new List<string> { },
|
||||
new List<string> { }));
|
||||
|
||||
var redis = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
|
||||
var options = ConfigurationOptions.Parse(redis);
|
||||
options.ClientName = "Lightless";
|
||||
options.ChannelPrefix = "UserData";
|
||||
ConnectionMultiplexer connectionMultiplexer = ConnectionMultiplexer.Connect(options);
|
||||
services.AddSingleton<IConnectionMultiplexer>(connectionMultiplexer);
|
||||
|
||||
services.Configure<ServicesConfiguration>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
services.Configure<LightlessConfigurationBase>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
services.AddSingleton(Configuration);
|
||||
services.AddSingleton<ServerTokenGenerator>();
|
||||
services.AddSingleton<DiscordBotServices>();
|
||||
services.AddHostedService<DiscordBot>();
|
||||
services.AddSingleton<IConfigurationService<ServicesConfiguration>, LightlessConfigurationServiceServer<ServicesConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, LightlessConfigurationServiceClient<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
||||
|
||||
services.AddHostedService(p => (LightlessConfigurationServiceClient<LightlessConfigurationBase>)p.GetService<IConfigurationService<LightlessConfigurationBase>>());
|
||||
services.AddHostedService(p => (LightlessConfigurationServiceClient<ServerConfiguration>)p.GetService<IConfigurationService<ServerConfiguration>>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
29
LightlessSyncServer/LightlessSyncServices/appsettings.json
Normal file
29
LightlessSyncServer/LightlessSyncServices/appsettings.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=;Username=;Password="
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Protocols": "Http2",
|
||||
"Url": "http://+:5002"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LightlessSync": {
|
||||
"DbContextPoolSize": 1024,
|
||||
"DiscordBotToken": "",
|
||||
"DiscordChannelForMessages": "",
|
||||
"PurgeUnusedAccounts": true,
|
||||
"PurgeUnusedAccountsPeriodInDays": 14,
|
||||
"FailedAuthForTempBan": 5,
|
||||
"TempBanDurationInMinutes": 30
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
145
LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs
Normal file
145
LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using LightlessSyncShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncShared.Data;
|
||||
|
||||
public class LightlessDbContext : DbContext
|
||||
{
|
||||
#if DEBUG
|
||||
public LightlessDbContext() { }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (optionsBuilder.IsConfigured)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
return;
|
||||
}
|
||||
|
||||
optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=;Username=;Password=", builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("LightlessSyncShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
optionsBuilder.EnableThreadSafetyChecks(false);
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
#endif
|
||||
|
||||
public LightlessDbContext(DbContextOptions<LightlessDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Auth> Auth { get; set; }
|
||||
public DbSet<BannedRegistrations> BannedRegistrations { get; set; }
|
||||
public DbSet<Banned> BannedUsers { get; set; }
|
||||
public DbSet<ClientPair> ClientPairs { get; set; }
|
||||
public DbSet<FileCache> Files { get; set; }
|
||||
public DbSet<ForbiddenUploadEntry> ForbiddenUploadEntries { get; set; }
|
||||
public DbSet<GroupBan> GroupBans { get; set; }
|
||||
public DbSet<GroupPair> GroupPairs { get; set; }
|
||||
public DbSet<Group> Groups { get; set; }
|
||||
public DbSet<GroupTempInvite> GroupTempInvites { get; set; }
|
||||
public DbSet<LodeStoneAuth> LodeStoneAuth { get; set; }
|
||||
public DbSet<UserProfileData> UserProfileData { get; set; }
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<UserPermissionSet> Permissions { get; set; }
|
||||
public DbSet<GroupPairPreferredPermission> GroupPairPreferredPermissions { get; set; }
|
||||
public DbSet<UserDefaultPreferredPermission> UserDefaultPreferredPermissions { get; set; }
|
||||
public DbSet<CharaData> CharaData { get; set; }
|
||||
public DbSet<CharaDataFile> CharaDataFiles { get; set; }
|
||||
public DbSet<CharaDataFileSwap> CharaDataFileSwaps { get; set; }
|
||||
public DbSet<CharaDataOriginalFile> CharaDataOriginalFiles { get; set; }
|
||||
public DbSet<CharaDataPose> CharaDataPoses { get; set; }
|
||||
public DbSet<CharaDataAllowance> CharaDataAllowances { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder mb)
|
||||
{
|
||||
mb.Entity<Auth>().ToTable("auth");
|
||||
mb.Entity<User>().ToTable("users");
|
||||
mb.Entity<FileCache>().ToTable("file_caches");
|
||||
mb.Entity<FileCache>().HasIndex(c => c.UploaderUID);
|
||||
mb.Entity<ClientPair>().ToTable("client_pairs");
|
||||
mb.Entity<ClientPair>().HasKey(u => new { u.UserUID, u.OtherUserUID });
|
||||
mb.Entity<ClientPair>().HasIndex(c => c.UserUID);
|
||||
mb.Entity<ClientPair>().HasIndex(c => c.OtherUserUID);
|
||||
mb.Entity<ForbiddenUploadEntry>().ToTable("forbidden_upload_entries");
|
||||
mb.Entity<Banned>().ToTable("banned_users");
|
||||
mb.Entity<LodeStoneAuth>().ToTable("lodestone_auth");
|
||||
mb.Entity<BannedRegistrations>().ToTable("banned_registrations");
|
||||
mb.Entity<Group>().ToTable("groups");
|
||||
mb.Entity<Group>().HasIndex(c => c.OwnerUID);
|
||||
mb.Entity<GroupPair>().ToTable("group_pairs");
|
||||
mb.Entity<GroupPair>().HasKey(u => new { u.GroupGID, u.GroupUserUID });
|
||||
mb.Entity<GroupPair>().HasIndex(c => c.GroupUserUID);
|
||||
mb.Entity<GroupPair>().HasIndex(c => c.GroupGID);
|
||||
mb.Entity<GroupBan>().ToTable("group_bans");
|
||||
mb.Entity<GroupBan>().HasKey(u => new { u.GroupGID, u.BannedUserUID });
|
||||
mb.Entity<GroupBan>().HasIndex(c => c.BannedUserUID);
|
||||
mb.Entity<GroupBan>().HasIndex(c => c.GroupGID);
|
||||
mb.Entity<GroupTempInvite>().ToTable("group_temp_invites");
|
||||
mb.Entity<GroupTempInvite>().HasKey(u => new { u.GroupGID, u.Invite });
|
||||
mb.Entity<GroupTempInvite>().HasIndex(c => c.GroupGID);
|
||||
mb.Entity<GroupTempInvite>().HasIndex(c => c.Invite);
|
||||
mb.Entity<UserProfileData>().ToTable("user_profile_data");
|
||||
mb.Entity<UserProfileData>().HasKey(c => c.UserUID);
|
||||
mb.Entity<UserPermissionSet>().ToTable("user_permission_sets");
|
||||
mb.Entity<UserPermissionSet>().HasKey(u => new { u.UserUID, u.OtherUserUID });
|
||||
mb.Entity<UserPermissionSet>().HasIndex(c => c.UserUID);
|
||||
mb.Entity<UserPermissionSet>().HasIndex(c => c.OtherUserUID);
|
||||
mb.Entity<UserPermissionSet>().HasIndex(c => new { c.UserUID, c.OtherUserUID, c.IsPaused });
|
||||
mb.Entity<GroupPairPreferredPermission>().ToTable("group_pair_preferred_permissions");
|
||||
mb.Entity<GroupPairPreferredPermission>().HasKey(u => new { u.UserUID, u.GroupGID });
|
||||
mb.Entity<GroupPairPreferredPermission>().HasIndex(c => c.UserUID);
|
||||
mb.Entity<GroupPairPreferredPermission>().HasIndex(c => c.GroupGID);
|
||||
mb.Entity<UserDefaultPreferredPermission>().ToTable("user_default_preferred_permissions");
|
||||
mb.Entity<UserDefaultPreferredPermission>().HasKey(u => u.UserUID);
|
||||
mb.Entity<UserDefaultPreferredPermission>().HasIndex(u => u.UserUID);
|
||||
mb.Entity<UserDefaultPreferredPermission>().HasOne(u => u.User);
|
||||
mb.Entity<CharaData>().ToTable("chara_data");
|
||||
mb.Entity<CharaData>()
|
||||
.HasMany(p => p.Poses)
|
||||
.WithOne(c => c.Parent)
|
||||
.HasForeignKey(c => new { c.ParentId, c.ParentUploaderUID });
|
||||
mb.Entity<CharaData>()
|
||||
.HasMany(p => p.Files)
|
||||
.WithOne(c => c.Parent)
|
||||
.HasForeignKey(c => new { c.ParentId, c.ParentUploaderUID });
|
||||
mb.Entity<CharaData>()
|
||||
.HasMany(p => p.OriginalFiles)
|
||||
.WithOne(p => p.Parent)
|
||||
.HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID });
|
||||
mb.Entity<CharaData>()
|
||||
.HasMany(p => p.AllowedIndividiuals)
|
||||
.WithOne(p => p.Parent)
|
||||
.HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID });
|
||||
mb.Entity<CharaData>()
|
||||
.HasMany(p => p.FileSwaps)
|
||||
.WithOne(p => p.Parent)
|
||||
.HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID });
|
||||
mb.Entity<CharaData>().HasKey(p => new { p.Id, p.UploaderUID });
|
||||
mb.Entity<CharaData>().HasIndex(p => p.UploaderUID);
|
||||
mb.Entity<CharaData>().HasIndex(p => p.Id);
|
||||
mb.Entity<CharaDataFile>().ToTable("chara_data_files");
|
||||
mb.Entity<CharaDataFile>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath });
|
||||
mb.Entity<CharaDataFile>().HasIndex(c => c.ParentId);
|
||||
mb.Entity<CharaDataFile>().HasOne(f => f.FileCache).WithMany().HasForeignKey(f => f.FileCacheHash).OnDelete(DeleteBehavior.Cascade);
|
||||
mb.Entity<CharaDataFileSwap>().ToTable("chara_data_file_swaps");
|
||||
mb.Entity<CharaDataFileSwap>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath });
|
||||
mb.Entity<CharaDataFileSwap>().HasIndex(c => c.ParentId);
|
||||
mb.Entity<CharaDataPose>().ToTable("chara_data_poses");
|
||||
mb.Entity<CharaDataPose>().Property(p => p.Id).ValueGeneratedOnAdd();
|
||||
mb.Entity<CharaDataPose>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.Id });
|
||||
mb.Entity<CharaDataPose>().HasIndex(c => c.ParentId);
|
||||
mb.Entity<CharaDataOriginalFile>().ToTable("chara_data_orig_files");
|
||||
mb.Entity<CharaDataOriginalFile>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath });
|
||||
mb.Entity<CharaDataOriginalFile>().HasIndex(c => c.ParentId);
|
||||
mb.Entity<CharaDataAllowance>().ToTable("chara_data_allowance");
|
||||
mb.Entity<CharaDataAllowance>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.Id });
|
||||
mb.Entity<CharaDataAllowance>().Property(p => p.Id).ValueGeneratedOnAdd();
|
||||
mb.Entity<CharaDataAllowance>().HasIndex(c => c.ParentId);
|
||||
mb.Entity<CharaDataAllowance>().HasOne(u => u.AllowedGroup).WithMany().HasForeignKey(u => u.AllowedGroupGID).OnDelete(DeleteBehavior.Cascade);
|
||||
mb.Entity<CharaDataAllowance>().HasOne(u => u.AllowedUser).WithMany().HasForeignKey(u => u.AllowedUserUID).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
39
LightlessSyncServer/LightlessSyncShared/Extensions.cs
Normal file
39
LightlessSyncServer/LightlessSyncShared/Extensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace LightlessSyncShared;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
private static long _noIpCntr = 0;
|
||||
public static string GetIpAddress(this IHttpContextAccessor accessor)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"]))
|
||||
return accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"];
|
||||
|
||||
if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["X-Forwarded-For"]))
|
||||
{
|
||||
return accessor.HttpContext.Request.Headers["X-Forwarded-For"];
|
||||
}
|
||||
|
||||
var ipAddress = accessor.HttpContext.GetServerVariable("HTTP_X_FORWARDED_FOR");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ipAddress))
|
||||
{
|
||||
var addresses = ipAddress.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
var lastEntry = addresses.LastOrDefault();
|
||||
if (lastEntry != null)
|
||||
{
|
||||
return lastEntry;
|
||||
}
|
||||
}
|
||||
|
||||
return accessor.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "NoIp";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "NoIp" + _noIpCntr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ByteSize" Version="2.1.2" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.6.0" />
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.184">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageReference Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
|
||||
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="10.2.0" />
|
||||
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="10.2.0" />
|
||||
<PackageReference Include="StackExchange.Redis.Extensions.System.Text.Json" Version="10.2.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,84 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Prometheus;
|
||||
|
||||
namespace LightlessSyncShared.Metrics;
|
||||
|
||||
public class LightlessMetrics
|
||||
{
|
||||
public LightlessMetrics(ILogger<LightlessMetrics> logger, List<string> countersToServe, List<string> gaugesToServe)
|
||||
{
|
||||
logger.LogInformation("Initializing LightlessMetrics");
|
||||
foreach (var counter in countersToServe)
|
||||
{
|
||||
logger.LogInformation($"Creating Metric for Counter {counter}");
|
||||
_counters.Add(counter, Prometheus.Metrics.CreateCounter(counter, counter));
|
||||
}
|
||||
|
||||
foreach (var gauge in gaugesToServe)
|
||||
{
|
||||
logger.LogInformation($"Creating Metric for Counter {gauge}");
|
||||
if (!string.Equals(gauge, MetricsAPI.GaugeConnections, StringComparison.OrdinalIgnoreCase))
|
||||
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge));
|
||||
else
|
||||
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, new[] { "continent" }));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Counter> _counters = new(StringComparer.Ordinal);
|
||||
|
||||
private readonly Dictionary<string, Gauge> _gauges = new(StringComparer.Ordinal);
|
||||
|
||||
public void IncGaugeWithLabels(string gaugeName, double value = 1.0, params string[] labels)
|
||||
{
|
||||
if (_gauges.TryGetValue(gaugeName, out Gauge gauge))
|
||||
{
|
||||
lock (gauge)
|
||||
gauge.WithLabels(labels).Inc(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void DecGaugeWithLabels(string gaugeName, double value = 1.0, params string[] labels)
|
||||
{
|
||||
if (_gauges.TryGetValue(gaugeName, out Gauge gauge))
|
||||
{
|
||||
lock (gauge)
|
||||
gauge.WithLabels(labels).Dec(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGaugeTo(string gaugeName, double value)
|
||||
{
|
||||
if (_gauges.TryGetValue(gaugeName, out Gauge gauge))
|
||||
{
|
||||
lock (gauge)
|
||||
gauge.Set(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void IncGauge(string gaugeName, double value = 1.0)
|
||||
{
|
||||
if (_gauges.TryGetValue(gaugeName, out Gauge gauge))
|
||||
{
|
||||
lock (gauge)
|
||||
gauge.Inc(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void DecGauge(string gaugeName, double value = 1.0)
|
||||
{
|
||||
if (_gauges.TryGetValue(gaugeName, out Gauge gauge))
|
||||
{
|
||||
lock (gauge)
|
||||
gauge.Dec(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void IncCounter(string counterName, double value = 1.0)
|
||||
{
|
||||
if (_counters.TryGetValue(counterName, out Counter counter))
|
||||
{
|
||||
lock (counter)
|
||||
counter.Inc(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace LightlessSyncShared.Metrics;
|
||||
|
||||
public class MetricsAPI
|
||||
{
|
||||
public const string CounterInitializedConnections = "lightless_initialized_connections";
|
||||
public const string GaugeConnections = "lightless_connections";
|
||||
public const string GaugeAuthorizedConnections = "lightless_authorized_connections";
|
||||
public const string GaugeAvailableWorkerThreads = "lightless_available_threadpool";
|
||||
public const string GaugeAvailableIOWorkerThreads = "lightless_available_threadpool_io";
|
||||
public const string GaugeUsersRegistered = "lightless_users_registered";
|
||||
public const string CounterUsersRegisteredDeleted = "lightless_users_registered_deleted";
|
||||
public const string GaugePairs = "lightless_pairs";
|
||||
public const string GaugePairsPaused = "lightless_pairs_paused";
|
||||
public const string GaugeFilesTotal = "lightless_files";
|
||||
public const string GaugeFilesTotalColdStorage = "lightless_files_cold";
|
||||
public const string GaugeFilesTotalSize = "lightless_files_size";
|
||||
public const string GaugeFilesTotalSizeColdStorage = "lightless_files_size_cold";
|
||||
public const string GaugeFilesDownloadingFromCache = "lightless_files_downloading_from_cache";
|
||||
public const string GaugeFilesTasksWaitingForDownloadFromCache = "lightless_files_waiting_for_dl";
|
||||
public const string CounterUserPushData = "lightless_user_push";
|
||||
public const string CounterUserPushDataTo = "lightless_user_push_to";
|
||||
public const string CounterAuthenticationRequests = "lightless_auth_requests";
|
||||
public const string CounterAuthenticationCacheHits = "lightless_auth_requests_cachehit";
|
||||
public const string CounterAuthenticationFailures = "lightless_auth_requests_fail";
|
||||
public const string CounterAuthenticationSuccesses = "lightless_auth_requests_success";
|
||||
public const string GaugeAuthenticationCacheEntries = "lightless_auth_cache";
|
||||
public const string GaugeGroups = "lightless_groups";
|
||||
public const string GaugeGroupPairs = "lightless_groups_pairs";
|
||||
public const string GaugeFilesUniquePastHour = "lightless_files_unique_past_hour";
|
||||
public const string GaugeFilesUniquePastHourSize = "lightless_files_unique_past_hour_size";
|
||||
public const string GaugeFilesUniquePastDay = "lightless_files_unique_past_day";
|
||||
public const string GaugeFilesUniquePastDaySize = "lightless_files_unique_past_day_size";
|
||||
public const string GaugeCurrentDownloads = "lightless_current_downloads";
|
||||
public const string GaugeQueueFree = "lightless_download_queue_free";
|
||||
public const string GaugeQueueActive = "lightless_download_queue_active";
|
||||
public const string GaugeQueueInactive = "lightless_download_queue_inactive";
|
||||
public const string GaugeDownloadQueue = "lightless_download_queue";
|
||||
public const string GaugeDownloadQueueCancelled = "lightless_download_queue_cancelled";
|
||||
public const string GaugeDownloadPriorityQueue = "lightless_download_priority_queue";
|
||||
public const string GaugeDownloadPriorityQueueCancelled = "lightless_download_priority_queue_cancelled";
|
||||
public const string CounterFileRequests = "lightless_files_requests";
|
||||
public const string CounterFileRequestSize = "lightless_files_request_size";
|
||||
public const string CounterUserPairCacheHit = "lightless_pairscache_hit";
|
||||
public const string CounterUserPairCacheMiss = "lightless_pairscache_miss";
|
||||
public const string GaugeUserPairCacheUsers = "lightless_pairscache_users";
|
||||
public const string GaugeUserPairCacheEntries = "lightless_pairscache_entries";
|
||||
public const string CounterUserPairCacheNewEntries = "lightless_pairscache_new_entries";
|
||||
public const string CounterUserPairCacheUpdatedEntries = "lightless_pairscache_updated_entries";
|
||||
public const string GaugeGposeLobbies = "lightless_gpose_lobbies";
|
||||
public const string GaugeGposeLobbyUsers = "lightless_gpose_lobby_users";
|
||||
public const string GaugeHubConcurrency = "lightless_free_concurrent_hub_calls";
|
||||
public const string GaugeHubQueuedConcurrency = "lightless_free_concurrent_queued_hub_calls";
|
||||
}
|
||||
241
LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.Designer.cs
generated
Normal file
241
LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,241 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220731210149_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("Auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("BannedUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("ClientPairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("FileCaches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("ForbiddenUploadEntries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("CharacterIdentification")
|
||||
.HasDatabaseName("ix_users_character_identification");
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_uid");
|
||||
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_uid");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BannedUsers",
|
||||
columns: table => new
|
||||
{
|
||||
character_identification = table.Column<string>(type: "text", nullable: false),
|
||||
reason = table.Column<string>(type: "text", nullable: true),
|
||||
timestamp = table.Column<byte[]>(type: "bytea", rowVersion: true, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_banned_users", x => x.character_identification);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ForbiddenUploadEntries",
|
||||
columns: table => new
|
||||
{
|
||||
hash = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
|
||||
forbidden_by = table.Column<string>(type: "text", nullable: true),
|
||||
timestamp = table.Column<byte[]>(type: "bytea", rowVersion: true, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_forbidden_upload_entries", x => x.hash);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
uid = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
character_identification = table.Column<string>(type: "text", nullable: true),
|
||||
timestamp = table.Column<byte[]>(type: "bytea", rowVersion: true, nullable: true),
|
||||
is_moderator = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_admin = table.Column<bool>(type: "boolean", nullable: false),
|
||||
last_logged_in = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_users", x => x.uid);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Auth",
|
||||
columns: table => new
|
||||
{
|
||||
hashed_key = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
user_uid = table.Column<string>(type: "character varying(10)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_auth", x => x.hashed_key);
|
||||
table.ForeignKey(
|
||||
name: "fk_auth_users_user_uid",
|
||||
column: x => x.user_uid,
|
||||
principalTable: "Users",
|
||||
principalColumn: "uid");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientPairs",
|
||||
columns: table => new
|
||||
{
|
||||
user_uid = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
other_user_uid = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
is_paused = table.Column<bool>(type: "boolean", nullable: false),
|
||||
allow_receiving_messages = table.Column<bool>(type: "boolean", nullable: false),
|
||||
timestamp = table.Column<byte[]>(type: "bytea", rowVersion: true, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_client_pairs", x => new { x.user_uid, x.other_user_uid });
|
||||
table.ForeignKey(
|
||||
name: "fk_client_pairs_users_other_user_uid",
|
||||
column: x => x.other_user_uid,
|
||||
principalTable: "Users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_client_pairs_users_user_uid",
|
||||
column: x => x.user_uid,
|
||||
principalTable: "Users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FileCaches",
|
||||
columns: table => new
|
||||
{
|
||||
hash = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
|
||||
uploader_uid = table.Column<string>(type: "character varying(10)", nullable: true),
|
||||
uploaded = table.Column<bool>(type: "boolean", nullable: false),
|
||||
timestamp = table.Column<byte[]>(type: "bytea", rowVersion: true, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_file_caches", x => x.hash);
|
||||
table.ForeignKey(
|
||||
name: "fk_file_caches_users_uploader_uid",
|
||||
column: x => x.uploader_uid,
|
||||
principalTable: "Users",
|
||||
principalColumn: "uid");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_user_uid",
|
||||
table: "Auth",
|
||||
column: "user_uid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_client_pairs_other_user_uid",
|
||||
table: "ClientPairs",
|
||||
column: "other_user_uid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_client_pairs_user_uid",
|
||||
table: "ClientPairs",
|
||||
column: "user_uid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_caches_uploader_uid",
|
||||
table: "FileCaches",
|
||||
column: "uploader_uid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_character_identification",
|
||||
table: "Users",
|
||||
column: "character_identification");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Auth");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BannedUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientPairs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "FileCaches");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ForbiddenUploadEntries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
241
LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs
generated
Normal file
241
LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs
generated
Normal file
@@ -0,0 +1,241 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220731211419_RenameLowerSnakeCase")]
|
||||
partial class RenameLowerSnakeCase
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("CharacterIdentification")
|
||||
.HasDatabaseName("ix_users_character_identification");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class RenameLowerSnakeCase : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_auth_users_user_uid",
|
||||
table: "Auth");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_client_pairs_users_other_user_uid",
|
||||
table: "ClientPairs");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_client_pairs_users_user_uid",
|
||||
table: "ClientPairs");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Users",
|
||||
newName: "users");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Auth",
|
||||
newName: "auth");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "ForbiddenUploadEntries",
|
||||
newName: "forbidden_upload_entries");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "FileCaches",
|
||||
newName: "file_caches");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "ClientPairs",
|
||||
newName: "client_pairs");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "BannedUsers",
|
||||
newName: "banned_users");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_auth_users_user_temp_id",
|
||||
table: "auth",
|
||||
column: "user_uid",
|
||||
principalTable: "users",
|
||||
principalColumn: "uid");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_client_pairs_users_other_user_temp_id1",
|
||||
table: "client_pairs",
|
||||
column: "other_user_uid",
|
||||
principalTable: "users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_client_pairs_users_user_temp_id2",
|
||||
table: "client_pairs",
|
||||
column: "user_uid",
|
||||
principalTable: "users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_auth_users_user_temp_id",
|
||||
table: "auth");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_client_pairs_users_other_user_temp_id1",
|
||||
table: "client_pairs");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_client_pairs_users_user_temp_id2",
|
||||
table: "client_pairs");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "users",
|
||||
newName: "Users");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "auth",
|
||||
newName: "Auth");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "forbidden_upload_entries",
|
||||
newName: "ForbiddenUploadEntries");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "file_caches",
|
||||
newName: "FileCaches");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "client_pairs",
|
||||
newName: "ClientPairs");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "banned_users",
|
||||
newName: "BannedUsers");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_auth_users_user_uid",
|
||||
table: "Auth",
|
||||
column: "user_uid",
|
||||
principalTable: "Users",
|
||||
principalColumn: "uid");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_client_pairs_users_other_user_uid",
|
||||
table: "ClientPairs",
|
||||
column: "other_user_uid",
|
||||
principalTable: "Users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_client_pairs_users_user_uid",
|
||||
table: "ClientPairs",
|
||||
column: "user_uid",
|
||||
principalTable: "Users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
283
LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs
generated
Normal file
283
LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs
generated
Normal file
@@ -0,0 +1,283 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220801121419_AddLodestoneAuth")]
|
||||
partial class AddLodestoneAuth
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("CharacterIdentification")
|
||||
.HasDatabaseName("ix_users_character_identification");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class AddLodestoneAuth : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "lodestone_auth",
|
||||
columns: table => new
|
||||
{
|
||||
discord_id = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
hashed_lodestone_id = table.Column<string>(type: "text", nullable: true),
|
||||
lodestone_auth_string = table.Column<string>(type: "text", nullable: true),
|
||||
user_uid = table.Column<string>(type: "character varying(10)", nullable: true),
|
||||
started_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_lodestone_auth", x => x.discord_id);
|
||||
table.ForeignKey(
|
||||
name: "fk_lodestone_auth_users_user_uid",
|
||||
column: x => x.user_uid,
|
||||
principalTable: "users",
|
||||
principalColumn: "uid");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_lodestone_auth_user_uid",
|
||||
table: "lodestone_auth",
|
||||
column: "user_uid");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "lodestone_auth");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220801122103_AddNullableLodestoneAuthProperties")]
|
||||
partial class AddNullableLodestoneAuthProperties
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("CharacterIdentification")
|
||||
.HasDatabaseName("ix_users_character_identification");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class AddNullableLodestoneAuthProperties : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "started_at",
|
||||
table: "lodestone_auth",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "started_at",
|
||||
table: "lodestone_auth",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
295
LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs
generated
Normal file
295
LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs
generated
Normal file
@@ -0,0 +1,295 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220806103053_AddBannedRegistrations")]
|
||||
partial class AddBannedRegistrations
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("CharacterIdentification")
|
||||
.HasDatabaseName("ix_users_character_identification");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class AddBannedRegistrations : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "banned_registrations",
|
||||
columns: table => new
|
||||
{
|
||||
discord_id_or_lodestone_auth = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_banned_registrations", x => x.discord_id_or_lodestone_auth);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "banned_registrations");
|
||||
}
|
||||
}
|
||||
}
|
||||
302
LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs
generated
Normal file
302
LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs
generated
Normal file
@@ -0,0 +1,302 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220816170426_SetMaxLimitForStrings")]
|
||||
partial class SetMaxLimitForStrings
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("CharacterIdentification")
|
||||
.HasDatabaseName("ix_users_character_identification");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncServer.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class SetMaxLimitForStrings : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "character_identification",
|
||||
table: "users",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "lodestone_auth_string",
|
||||
table: "lodestone_auth",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "hashed_lodestone_id",
|
||||
table: "lodestone_auth",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "forbidden_by",
|
||||
table: "forbidden_upload_entries",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "character_identification",
|
||||
table: "banned_users",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "discord_id_or_lodestone_auth",
|
||||
table: "banned_registrations",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "character_identification",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "lodestone_auth_string",
|
||||
table: "lodestone_auth",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "hashed_lodestone_id",
|
||||
table: "lodestone_auth",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "forbidden_by",
|
||||
table: "forbidden_upload_entries",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "character_identification",
|
||||
table: "banned_users",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "discord_id_or_lodestone_auth",
|
||||
table: "banned_registrations",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
307
LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.Designer.cs
generated
Normal file
307
LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.Designer.cs
generated
Normal file
@@ -0,0 +1,307 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220824225157_AddAlias")]
|
||||
partial class AddAlias
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("CharacterIdentification")
|
||||
.HasDatabaseName("ix_users_character_identification");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class AddAlias : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "alias",
|
||||
table: "users",
|
||||
type: "character varying(10)",
|
||||
maxLength: 10,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "alias",
|
||||
table: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
389
LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.Designer.cs
generated
Normal file
389
LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.Designer.cs
generated
Normal file
@@ -0,0 +1,389 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220917115233_Groups")]
|
||||
partial class Groups
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.Property<string>("GID")
|
||||
.HasMaxLength(14)
|
||||
.HasColumnType("character varying(14)")
|
||||
.HasColumnName("gid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_password");
|
||||
|
||||
b.Property<bool>("InvitesEnabled")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("invites_enabled");
|
||||
|
||||
b.Property<string>("OwnerUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("owner_uid");
|
||||
|
||||
b.HasKey("GID")
|
||||
.HasName("pk_groups");
|
||||
|
||||
b.HasIndex("OwnerUID")
|
||||
.HasDatabaseName("ix_groups_owner_uid");
|
||||
|
||||
b.ToTable("groups", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.Property<string>("GroupGID")
|
||||
.HasColumnType("character varying(14)")
|
||||
.HasColumnName("group_gid");
|
||||
|
||||
b.Property<string>("GroupUserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("group_user_uid");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.HasKey("GroupGID", "GroupUserUID")
|
||||
.HasName("pk_group_pairs");
|
||||
|
||||
b.HasIndex("GroupGID")
|
||||
.HasDatabaseName("ix_group_pairs_group_gid");
|
||||
|
||||
b.HasIndex("GroupUserUID")
|
||||
.HasDatabaseName("ix_group_pairs_group_user_uid");
|
||||
|
||||
b.ToTable("group_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUID")
|
||||
.HasConstraintName("fk_groups_users_owner_temp_id5");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupGID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_groups_group_temp_id");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "GroupUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_users_group_user_temp_id4");
|
||||
|
||||
b.Navigation("Group");
|
||||
|
||||
b.Navigation("GroupUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class Groups : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_users_character_identification",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "character_identification",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "alias",
|
||||
table: "users",
|
||||
type: "character varying(10)",
|
||||
maxLength: 10,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "groups",
|
||||
columns: table => new
|
||||
{
|
||||
gid = table.Column<string>(type: "character varying(14)", maxLength: 14, nullable: false),
|
||||
owner_uid = table.Column<string>(type: "character varying(10)", nullable: true),
|
||||
alias = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
invites_enabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
hashed_password = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_groups", x => x.gid);
|
||||
table.ForeignKey(
|
||||
name: "fk_groups_users_owner_temp_id5",
|
||||
column: x => x.owner_uid,
|
||||
principalTable: "users",
|
||||
principalColumn: "uid");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "group_pairs",
|
||||
columns: table => new
|
||||
{
|
||||
group_gid = table.Column<string>(type: "character varying(14)", nullable: false),
|
||||
group_user_uid = table.Column<string>(type: "character varying(10)", nullable: false),
|
||||
is_paused = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_group_pairs", x => new { x.group_gid, x.group_user_uid });
|
||||
table.ForeignKey(
|
||||
name: "fk_group_pairs_groups_group_temp_id",
|
||||
column: x => x.group_gid,
|
||||
principalTable: "groups",
|
||||
principalColumn: "gid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_group_pairs_users_group_user_temp_id4",
|
||||
column: x => x.group_user_uid,
|
||||
principalTable: "users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_group_pairs_group_gid",
|
||||
table: "group_pairs",
|
||||
column: "group_gid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_group_pairs_group_user_uid",
|
||||
table: "group_pairs",
|
||||
column: "group_user_uid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_groups_owner_uid",
|
||||
table: "groups",
|
||||
column: "owner_uid");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "group_pairs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "groups");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "alias",
|
||||
table: "users",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(10)",
|
||||
oldMaxLength: 10,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "character_identification",
|
||||
table: "users",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_character_identification",
|
||||
table: "users",
|
||||
column: "character_identification");
|
||||
}
|
||||
}
|
||||
}
|
||||
389
LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.Designer.cs
generated
Normal file
389
LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.Designer.cs
generated
Normal file
@@ -0,0 +1,389 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20220929150304_ChangeGidLength")]
|
||||
partial class ChangeGidLength
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.Property<string>("GID")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("gid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_password");
|
||||
|
||||
b.Property<bool>("InvitesEnabled")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("invites_enabled");
|
||||
|
||||
b.Property<string>("OwnerUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("owner_uid");
|
||||
|
||||
b.HasKey("GID")
|
||||
.HasName("pk_groups");
|
||||
|
||||
b.HasIndex("OwnerUID")
|
||||
.HasDatabaseName("ix_groups_owner_uid");
|
||||
|
||||
b.ToTable("groups", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.Property<string>("GroupGID")
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("group_gid");
|
||||
|
||||
b.Property<string>("GroupUserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("group_user_uid");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.HasKey("GroupGID", "GroupUserUID")
|
||||
.HasName("pk_group_pairs");
|
||||
|
||||
b.HasIndex("GroupGID")
|
||||
.HasDatabaseName("ix_group_pairs_group_gid");
|
||||
|
||||
b.HasIndex("GroupUserUID")
|
||||
.HasDatabaseName("ix_group_pairs_group_user_uid");
|
||||
|
||||
b.ToTable("group_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUID")
|
||||
.HasConstraintName("fk_groups_users_owner_temp_id5");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupGID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_groups_group_temp_id");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "GroupUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_users_group_user_temp_id4");
|
||||
|
||||
b.Navigation("Group");
|
||||
|
||||
b.Navigation("GroupUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class ChangeGidLength : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "gid",
|
||||
table: "groups",
|
||||
type: "character varying(20)",
|
||||
maxLength: 20,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(14)",
|
||||
oldMaxLength: 14);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "group_gid",
|
||||
table: "group_pairs",
|
||||
type: "character varying(20)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(14)");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "gid",
|
||||
table: "groups",
|
||||
type: "character varying(14)",
|
||||
maxLength: 14,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(20)",
|
||||
oldMaxLength: 20);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "group_gid",
|
||||
table: "group_pairs",
|
||||
type: "character varying(14)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(20)");
|
||||
}
|
||||
}
|
||||
}
|
||||
393
LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.Designer.cs
generated
Normal file
393
LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.Designer.cs
generated
Normal file
@@ -0,0 +1,393 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20221002105428_IsPinned")]
|
||||
partial class IsPinned
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.Property<string>("GID")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("gid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_password");
|
||||
|
||||
b.Property<bool>("InvitesEnabled")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("invites_enabled");
|
||||
|
||||
b.Property<string>("OwnerUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("owner_uid");
|
||||
|
||||
b.HasKey("GID")
|
||||
.HasName("pk_groups");
|
||||
|
||||
b.HasIndex("OwnerUID")
|
||||
.HasDatabaseName("ix_groups_owner_uid");
|
||||
|
||||
b.ToTable("groups", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.Property<string>("GroupGID")
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("group_gid");
|
||||
|
||||
b.Property<string>("GroupUserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("group_user_uid");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_pinned");
|
||||
|
||||
b.HasKey("GroupGID", "GroupUserUID")
|
||||
.HasName("pk_group_pairs");
|
||||
|
||||
b.HasIndex("GroupGID")
|
||||
.HasDatabaseName("ix_group_pairs_group_gid");
|
||||
|
||||
b.HasIndex("GroupUserUID")
|
||||
.HasDatabaseName("ix_group_pairs_group_user_uid");
|
||||
|
||||
b.ToTable("group_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUID")
|
||||
.HasConstraintName("fk_groups_users_owner_temp_id5");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupGID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_groups_group_temp_id");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "GroupUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_users_group_user_temp_id4");
|
||||
|
||||
b.Navigation("Group");
|
||||
|
||||
b.Navigation("GroupUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class IsPinned : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_pinned",
|
||||
table: "group_pairs",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_pinned",
|
||||
table: "group_pairs");
|
||||
}
|
||||
}
|
||||
}
|
||||
393
LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs
generated
Normal file
393
LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs
generated
Normal file
@@ -0,0 +1,393 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20221004125939_AdjustAliasLength")]
|
||||
partial class AdjustAliasLength
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.Property<string>("GID")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("gid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_password");
|
||||
|
||||
b.Property<bool>("InvitesEnabled")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("invites_enabled");
|
||||
|
||||
b.Property<string>("OwnerUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("owner_uid");
|
||||
|
||||
b.HasKey("GID")
|
||||
.HasName("pk_groups");
|
||||
|
||||
b.HasIndex("OwnerUID")
|
||||
.HasDatabaseName("ix_groups_owner_uid");
|
||||
|
||||
b.ToTable("groups", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.Property<string>("GroupGID")
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("group_gid");
|
||||
|
||||
b.Property<string>("GroupUserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("group_user_uid");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_pinned");
|
||||
|
||||
b.HasKey("GroupGID", "GroupUserUID")
|
||||
.HasName("pk_group_pairs");
|
||||
|
||||
b.HasIndex("GroupGID")
|
||||
.HasDatabaseName("ix_group_pairs_group_gid");
|
||||
|
||||
b.HasIndex("GroupUserUID")
|
||||
.HasDatabaseName("ix_group_pairs_group_user_uid");
|
||||
|
||||
b.ToTable("group_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(15)
|
||||
.HasColumnType("character varying(15)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUID")
|
||||
.HasConstraintName("fk_groups_users_owner_temp_id5");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupGID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_groups_group_temp_id");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "GroupUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_users_group_user_temp_id4");
|
||||
|
||||
b.Navigation("Group");
|
||||
|
||||
b.Navigation("GroupUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class AdjustAliasLength : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "alias",
|
||||
table: "users",
|
||||
type: "character varying(15)",
|
||||
maxLength: 15,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(10)",
|
||||
oldMaxLength: 10,
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "alias",
|
||||
table: "users",
|
||||
type: "character varying(10)",
|
||||
maxLength: 10,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(15)",
|
||||
oldMaxLength: 15,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
397
LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.Designer.cs
generated
Normal file
397
LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.Designer.cs
generated
Normal file
@@ -0,0 +1,397 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20221006115929_GroupModerator")]
|
||||
partial class GroupModerator
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.Property<string>("GID")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("gid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_password");
|
||||
|
||||
b.Property<bool>("InvitesEnabled")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("invites_enabled");
|
||||
|
||||
b.Property<string>("OwnerUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("owner_uid");
|
||||
|
||||
b.HasKey("GID")
|
||||
.HasName("pk_groups");
|
||||
|
||||
b.HasIndex("OwnerUID")
|
||||
.HasDatabaseName("ix_groups_owner_uid");
|
||||
|
||||
b.ToTable("groups", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.Property<string>("GroupGID")
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("group_gid");
|
||||
|
||||
b.Property<string>("GroupUserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("group_user_uid");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_pinned");
|
||||
|
||||
b.HasKey("GroupGID", "GroupUserUID")
|
||||
.HasName("pk_group_pairs");
|
||||
|
||||
b.HasIndex("GroupGID")
|
||||
.HasDatabaseName("ix_group_pairs_group_gid");
|
||||
|
||||
b.HasIndex("GroupUserUID")
|
||||
.HasDatabaseName("ix_group_pairs_group_user_uid");
|
||||
|
||||
b.ToTable("group_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(15)
|
||||
.HasColumnType("character varying(15)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUID")
|
||||
.HasConstraintName("fk_groups_users_owner_temp_id5");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupGID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_groups_group_temp_id");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "GroupUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_users_group_user_temp_id4");
|
||||
|
||||
b.Navigation("Group");
|
||||
|
||||
b.Navigation("GroupUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class GroupModerator : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_moderator",
|
||||
table: "group_pairs",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_moderator",
|
||||
table: "group_pairs");
|
||||
}
|
||||
}
|
||||
}
|
||||
462
LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.Designer.cs
generated
Normal file
462
LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.Designer.cs
generated
Normal file
@@ -0,0 +1,462 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LightlessDbContext))]
|
||||
[Migration("20221006122618_groupbans")]
|
||||
partial class groupbans
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.Property<string>("HashedKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("hashed_key");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("HashedKey")
|
||||
.HasName("pk_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_auth_user_uid");
|
||||
|
||||
b.ToTable("auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Banned", b =>
|
||||
{
|
||||
b.Property<string>("CharacterIdentification")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("character_identification");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("CharacterIdentification")
|
||||
.HasName("pk_banned_users");
|
||||
|
||||
b.ToTable("banned_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b =>
|
||||
{
|
||||
b.Property<string>("DiscordIdOrLodestoneAuth")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("discord_id_or_lodestone_auth");
|
||||
|
||||
b.HasKey("DiscordIdOrLodestoneAuth")
|
||||
.HasName("pk_banned_registrations");
|
||||
|
||||
b.ToTable("banned_registrations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.Property<string>("UserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.Property<string>("OtherUserUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("other_user_uid");
|
||||
|
||||
b.Property<bool>("AllowReceivingMessages")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_receiving_messages");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UserUID", "OtherUserUID")
|
||||
.HasName("pk_client_pairs");
|
||||
|
||||
b.HasIndex("OtherUserUID")
|
||||
.HasDatabaseName("ix_client_pairs_other_user_uid");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_client_pairs_user_uid");
|
||||
|
||||
b.ToTable("client_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<bool>("Uploaded")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("uploaded");
|
||||
|
||||
b.Property<string>("UploaderUID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uploader_uid");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_file_caches");
|
||||
|
||||
b.HasIndex("UploaderUID")
|
||||
.HasDatabaseName("ix_file_caches_uploader_uid");
|
||||
|
||||
b.ToTable("file_caches", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b =>
|
||||
{
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("ForbiddenBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("forbidden_by");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Hash")
|
||||
.HasName("pk_forbidden_upload_entries");
|
||||
|
||||
b.ToTable("forbidden_upload_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.Property<string>("GID")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("gid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_password");
|
||||
|
||||
b.Property<bool>("InvitesEnabled")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("invites_enabled");
|
||||
|
||||
b.Property<string>("OwnerUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("owner_uid");
|
||||
|
||||
b.HasKey("GID")
|
||||
.HasName("pk_groups");
|
||||
|
||||
b.HasIndex("OwnerUID")
|
||||
.HasDatabaseName("ix_groups_owner_uid");
|
||||
|
||||
b.ToTable("groups", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b =>
|
||||
{
|
||||
b.Property<string>("GroupGID")
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("group_gid");
|
||||
|
||||
b.Property<string>("BannedUserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("banned_user_uid");
|
||||
|
||||
b.Property<string>("BannedByUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("banned_by_uid");
|
||||
|
||||
b.Property<DateTime>("BannedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("banned_on");
|
||||
|
||||
b.Property<string>("BannedReason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("banned_reason");
|
||||
|
||||
b.HasKey("GroupGID", "BannedUserUID")
|
||||
.HasName("pk_group_bans");
|
||||
|
||||
b.HasIndex("BannedByUID")
|
||||
.HasDatabaseName("ix_group_bans_banned_by_uid");
|
||||
|
||||
b.HasIndex("BannedUserUID")
|
||||
.HasDatabaseName("ix_group_bans_banned_user_uid");
|
||||
|
||||
b.HasIndex("GroupGID")
|
||||
.HasDatabaseName("ix_group_bans_group_gid");
|
||||
|
||||
b.ToTable("group_bans", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.Property<string>("GroupGID")
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("group_gid");
|
||||
|
||||
b.Property<string>("GroupUserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("group_user_uid");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<bool>("IsPaused")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_paused");
|
||||
|
||||
b.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_pinned");
|
||||
|
||||
b.HasKey("GroupGID", "GroupUserUID")
|
||||
.HasName("pk_group_pairs");
|
||||
|
||||
b.HasIndex("GroupGID")
|
||||
.HasDatabaseName("ix_group_pairs_group_gid");
|
||||
|
||||
b.HasIndex("GroupUserUID")
|
||||
.HasDatabaseName("ix_group_pairs_group_user_uid");
|
||||
|
||||
b.ToTable("group_pairs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.Property<decimal>("DiscordId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("HashedLodestoneId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("hashed_lodestone_id");
|
||||
|
||||
b.Property<string>("LodestoneAuthString")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("lodestone_auth_string");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("UserUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("user_uid");
|
||||
|
||||
b.HasKey("DiscordId")
|
||||
.HasName("pk_lodestone_auth");
|
||||
|
||||
b.HasIndex("UserUID")
|
||||
.HasDatabaseName("ix_lodestone_auth_user_uid");
|
||||
|
||||
b.ToTable("lodestone_auth", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("uid");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(15)
|
||||
.HasColumnType("character varying(15)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_admin");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_moderator");
|
||||
|
||||
b.Property<DateTime>("LastLoggedIn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_logged_in");
|
||||
|
||||
b.Property<byte[]>("Timestamp")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("UID")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Auth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_auth_users_user_temp_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "OtherUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OtherUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_other_user_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_client_pairs_users_user_temp_id2");
|
||||
|
||||
b.Navigation("OtherUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Uploader")
|
||||
.WithMany()
|
||||
.HasForeignKey("UploaderUID")
|
||||
.HasConstraintName("fk_file_caches_users_uploader_uid");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.Group", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUID")
|
||||
.HasConstraintName("fk_groups_users_owner_temp_id7");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "BannedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("BannedByUID")
|
||||
.HasConstraintName("fk_group_bans_users_banned_by_temp_id4");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "BannedUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("BannedUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_bans_users_banned_user_temp_id5");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupGID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_bans_groups_group_temp_id");
|
||||
|
||||
b.Navigation("BannedBy");
|
||||
|
||||
b.Navigation("BannedUser");
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupGID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_groups_group_temp_id1");
|
||||
|
||||
b.HasOne("LightlessSyncShared.Models.User", "GroupUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupUserUID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_group_pairs_users_group_user_temp_id6");
|
||||
|
||||
b.Navigation("Group");
|
||||
|
||||
b.Navigation("GroupUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b =>
|
||||
{
|
||||
b.HasOne("LightlessSyncShared.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserUID")
|
||||
.HasConstraintName("fk_lodestone_auth_users_user_uid");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LightlessSyncServer.Migrations
|
||||
{
|
||||
public partial class groupbans : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_group_pairs_groups_group_temp_id",
|
||||
table: "group_pairs");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_group_pairs_users_group_user_temp_id4",
|
||||
table: "group_pairs");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_groups_users_owner_temp_id5",
|
||||
table: "groups");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "group_bans",
|
||||
columns: table => new
|
||||
{
|
||||
group_gid = table.Column<string>(type: "character varying(20)", nullable: false),
|
||||
banned_user_uid = table.Column<string>(type: "character varying(10)", nullable: false),
|
||||
banned_by_uid = table.Column<string>(type: "character varying(10)", nullable: true),
|
||||
banned_on = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
banned_reason = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_group_bans", x => new { x.group_gid, x.banned_user_uid });
|
||||
table.ForeignKey(
|
||||
name: "fk_group_bans_groups_group_temp_id",
|
||||
column: x => x.group_gid,
|
||||
principalTable: "groups",
|
||||
principalColumn: "gid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_group_bans_users_banned_by_temp_id4",
|
||||
column: x => x.banned_by_uid,
|
||||
principalTable: "users",
|
||||
principalColumn: "uid");
|
||||
table.ForeignKey(
|
||||
name: "fk_group_bans_users_banned_user_temp_id5",
|
||||
column: x => x.banned_user_uid,
|
||||
principalTable: "users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_group_bans_banned_by_uid",
|
||||
table: "group_bans",
|
||||
column: "banned_by_uid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_group_bans_banned_user_uid",
|
||||
table: "group_bans",
|
||||
column: "banned_user_uid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_group_bans_group_gid",
|
||||
table: "group_bans",
|
||||
column: "group_gid");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_group_pairs_groups_group_temp_id1",
|
||||
table: "group_pairs",
|
||||
column: "group_gid",
|
||||
principalTable: "groups",
|
||||
principalColumn: "gid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_group_pairs_users_group_user_temp_id6",
|
||||
table: "group_pairs",
|
||||
column: "group_user_uid",
|
||||
principalTable: "users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_groups_users_owner_temp_id7",
|
||||
table: "groups",
|
||||
column: "owner_uid",
|
||||
principalTable: "users",
|
||||
principalColumn: "uid");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_group_pairs_groups_group_temp_id1",
|
||||
table: "group_pairs");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_group_pairs_users_group_user_temp_id6",
|
||||
table: "group_pairs");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_groups_users_owner_temp_id7",
|
||||
table: "groups");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "group_bans");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_group_pairs_groups_group_temp_id",
|
||||
table: "group_pairs",
|
||||
column: "group_gid",
|
||||
principalTable: "groups",
|
||||
principalColumn: "gid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_group_pairs_users_group_user_temp_id4",
|
||||
table: "group_pairs",
|
||||
column: "group_user_uid",
|
||||
principalTable: "users",
|
||||
principalColumn: "uid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_groups_users_owner_temp_id5",
|
||||
table: "groups",
|
||||
column: "owner_uid",
|
||||
principalTable: "users",
|
||||
principalColumn: "uid");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user