✨ Done mixing
This commit is contained in:
190
DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.Designer.cs
generated
Normal file
190
DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.Designer.cs
generated
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DysonNetwork.Drive;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NodaTime;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Drive.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDatabase))]
|
||||||
|
[Migration("20250715080004_ReinitalMigration")]
|
||||||
|
partial class ReinitalMigration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("FileMeta")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("file_meta");
|
||||||
|
|
||||||
|
b.Property<bool>("HasCompression")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("has_compression");
|
||||||
|
|
||||||
|
b.Property<string>("Hash")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("hash");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMarkedRecycle")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_marked_recycle");
|
||||||
|
|
||||||
|
b.Property<string>("MimeType")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("mime_type");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("sensitive_marks");
|
||||||
|
|
||||||
|
b.Property<long>("Size")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("size");
|
||||||
|
|
||||||
|
b.Property<string>("StorageId")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("storage_id");
|
||||||
|
|
||||||
|
b.Property<string>("StorageUrl")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("storage_url");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("UploadedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("uploaded_at");
|
||||||
|
|
||||||
|
b.Property<string>("UploadedTo")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("uploaded_to");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("UserMeta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("user_meta");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_files");
|
||||||
|
|
||||||
|
b.ToTable("files", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("file_id");
|
||||||
|
|
||||||
|
b.Property<string>("ResourceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("resource_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Usage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("usage");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_file_references");
|
||||||
|
|
||||||
|
b.HasIndex("FileId")
|
||||||
|
.HasDatabaseName("ix_file_references_file_id");
|
||||||
|
|
||||||
|
b.ToTable("file_references", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FileId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_file_references_files_file_id");
|
||||||
|
|
||||||
|
b.Navigation("File");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Drive.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ReinitalMigration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<Dictionary<string, object>>(
|
||||||
|
name: "file_meta",
|
||||||
|
table: "files",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(Dictionary<string, object>),
|
||||||
|
oldType: "jsonb",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<Dictionary<string, object>>(
|
||||||
|
name: "file_meta",
|
||||||
|
table: "files",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(Dictionary<string, object>),
|
||||||
|
oldType: "jsonb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using DysonNetwork.Drive;
|
using DysonNetwork.Drive;
|
||||||
using DysonNetwork.Drive.Storage;
|
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -52,6 +51,7 @@ namespace DysonNetwork.Drive.Migrations
|
|||||||
.HasColumnName("description");
|
.HasColumnName("description");
|
||||||
|
|
||||||
b.Property<Dictionary<string, object>>("FileMeta")
|
b.Property<Dictionary<string, object>>("FileMeta")
|
||||||
|
.IsRequired()
|
||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("file_meta");
|
.HasColumnName("file_meta");
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ builder.Services.AddAppServices(builder.Configuration);
|
|||||||
builder.Services.AddAppRateLimiting();
|
builder.Services.AddAppRateLimiting();
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddAppSwagger();
|
builder.Services.AddAppSwagger();
|
||||||
builder.Services.AddDysonAuth(builder.Configuration);
|
builder.Services.AddDysonAuth();
|
||||||
|
|
||||||
builder.Services.AddAppFileStorage(builder.Configuration);
|
builder.Services.AddAppFileStorage(builder.Configuration);
|
||||||
|
|
||||||
|
1967
DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.Designer.cs
generated
Normal file
1967
DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ReinitalMigration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "background_id",
|
||||||
|
table: "account_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "picture_id",
|
||||||
|
table: "account_profiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "background_id",
|
||||||
|
table: "account_profiles",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "picture_id",
|
||||||
|
table: "account_profiles",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -392,11 +392,6 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("background");
|
.HasColumnName("background");
|
||||||
|
|
||||||
b.Property<string>("BackgroundId")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)")
|
|
||||||
.HasColumnName("background_id");
|
|
||||||
|
|
||||||
b.Property<string>("Bio")
|
b.Property<string>("Bio")
|
||||||
.HasMaxLength(4096)
|
.HasMaxLength(4096)
|
||||||
.HasColumnType("character varying(4096)")
|
.HasColumnType("character varying(4096)")
|
||||||
@@ -451,11 +446,6 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("picture");
|
.HasColumnName("picture");
|
||||||
|
|
||||||
b.Property<string>("PictureId")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)")
|
|
||||||
.HasColumnName("picture_id");
|
|
||||||
|
|
||||||
b.Property<string>("Pronouns")
|
b.Property<string>("Pronouns")
|
||||||
.HasMaxLength(1024)
|
.HasMaxLength(1024)
|
||||||
.HasColumnType("character varying(1024)")
|
.HasColumnType("character varying(1024)")
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using DysonNetwork.Pass;
|
using DysonNetwork.Pass;
|
||||||
using DysonNetwork.Pass.Account;
|
|
||||||
using DysonNetwork.Pass.Startup;
|
using DysonNetwork.Pass.Startup;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Pass.Account;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Safety;
|
namespace DysonNetwork.Pass.Safety;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/safety/reports")]
|
[Route("/api/safety/reports")]
|
||||||
@@ -30,7 +30,7 @@ public class AbuseReportController(
|
|||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<AbuseReport>> CreateReport([FromBody] CreateReportRequest request)
|
public async Task<ActionResult<AbuseReport>> CreateReport([FromBody] CreateReportRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -75,7 +75,7 @@ public class AbuseReportController(
|
|||||||
[FromQuery] bool includeResolved = false
|
[FromQuery] bool includeResolved = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var totalCount = await safety.CountUserReports(currentUser.Id, includeResolved);
|
var totalCount = await safety.CountUserReports(currentUser.Id, includeResolved);
|
||||||
var reports = await safety.GetUserReports(currentUser.Id, offset, take, includeResolved);
|
var reports = await safety.GetUserReports(currentUser.Id, offset, take, includeResolved);
|
||||||
@@ -101,7 +101,7 @@ public class AbuseReportController(
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult<AbuseReport>> GetMyReportById(Guid id)
|
public async Task<ActionResult<AbuseReport>> GetMyReportById(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var report = await safety.GetReportById(id);
|
var report = await safety.GetReportById(id);
|
||||||
if (report == null) return NotFound();
|
if (report == null) return NotFound();
|
@@ -1,8 +1,8 @@
|
|||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Pass.Account;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Safety;
|
namespace DysonNetwork.Pass.Safety;
|
||||||
|
|
||||||
public class SafetyService(AppDatabase db, ILogger<SafetyService> logger)
|
public class SafetyService(AppDatabase db, ILogger<SafetyService> logger)
|
||||||
{
|
{
|
@@ -217,7 +217,7 @@ public class PaymentService(
|
|||||||
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
||||||
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
|
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
|
||||||
readableOrderRemark],
|
readableOrderRemark],
|
||||||
IsSavable = false
|
IsSavable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -360,7 +360,7 @@ public class SubscriptionService(
|
|||||||
Topic = "subscriptions.begun",
|
Topic = "subscriptions.begun",
|
||||||
Title = localizer["SubscriptionAppliedTitle", humanReadableName],
|
Title = localizer["SubscriptionAppliedTitle", humanReadableName],
|
||||||
Body = localizer["SubscriptionAppliedBody", duration, humanReadableName],
|
Body = localizer["SubscriptionAppliedBody", duration, humanReadableName],
|
||||||
IsSavable = false,
|
IsSavable = true
|
||||||
};
|
};
|
||||||
notification.Meta.Add("subscription_id", Value.ForString(subscription.Id.ToString()));
|
notification.Meta.Add("subscription_id", Value.ForString(subscription.Id.ToString()));
|
||||||
await pusher.SendPushNotificationToUserAsync(
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
|
@@ -15,7 +15,7 @@ builder.Services.AddAppServices(builder.Configuration);
|
|||||||
builder.Services.AddAppRateLimiting();
|
builder.Services.AddAppRateLimiting();
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddAppSwagger();
|
builder.Services.AddAppSwagger();
|
||||||
builder.Services.AddDysonAuth(builder.Configuration);
|
builder.Services.AddDysonAuth();
|
||||||
|
|
||||||
// Add flush handlers and websocket handlers
|
// Add flush handlers and websocket handlers
|
||||||
builder.Services.AddAppFlushHandlers();
|
builder.Services.AddAppFlushHandlers();
|
||||||
|
@@ -8,8 +8,7 @@ namespace DysonNetwork.Shared.Auth;
|
|||||||
public static class DysonAuthStartup
|
public static class DysonAuthStartup
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddDysonAuth(
|
public static IServiceCollection AddDysonAuth(
|
||||||
this IServiceCollection services,
|
this IServiceCollection services
|
||||||
IConfiguration configuration
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
services.AddSingleton<AuthService.AuthServiceClient>(sp =>
|
services.AddSingleton<AuthService.AuthServiceClient>(sp =>
|
||||||
|
@@ -74,7 +74,7 @@ public class ChatMember : ModelBase
|
|||||||
public Guid ChatRoomId { get; set; }
|
public Guid ChatRoomId { get; set; }
|
||||||
public ChatRoom ChatRoom { get; set; } = null!;
|
public ChatRoom ChatRoom { get; set; } = null!;
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public Account Account { get; set; } = null!;
|
[NotMapped] public Account Account { get; set; } = null!;
|
||||||
|
|
||||||
[MaxLength(1024)] public string? Nick { get; set; }
|
[MaxLength(1024)] public string? Nick { get; set; }
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ public class ChatMemberTransmissionObject : ModelBase
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid ChatRoomId { get; set; }
|
public Guid ChatRoomId { get; set; }
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public Account Account { get; set; } = null!;
|
[NotMapped] public Account Account { get; set; } = null!;
|
||||||
|
|
||||||
[MaxLength(1024)] public string? Nick { get; set; }
|
[MaxLength(1024)] public string? Nick { get; set; }
|
||||||
|
|
||||||
|
@@ -2,10 +2,10 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Shared;
|
using DysonNetwork.Shared;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Realm;
|
using DysonNetwork.Sphere.Realm;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -962,7 +962,7 @@ public class ChatRoomController(
|
|||||||
Topic = "invites.chats",
|
Topic = "invites.chats",
|
||||||
Title = title,
|
Title = title,
|
||||||
Body = body,
|
Body = body,
|
||||||
IsSavable = false
|
IsSavable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -1,947 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using DysonNetwork.Shared.Data;
|
|
||||||
using DysonNetwork.Shared.Proto;
|
|
||||||
using DysonNetwork.Sphere.Localization;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Realm;
|
|
||||||
using Grpc.Core;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("/api/chat")]
|
|
||||||
public class ChatRoomController(
|
|
||||||
AppDatabase db,
|
|
||||||
ChatRoomService crs,
|
|
||||||
RealmService rs,
|
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
|
||||||
AccountService.AccountServiceClient accounts,
|
|
||||||
FileService.FileServiceClient files,
|
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
|
||||||
ActionLogService.ActionLogServiceClient als
|
|
||||||
) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet("{id:guid}")]
|
|
||||||
public async Task<ActionResult<ChatRoom>> GetChatRoom(Guid id)
|
|
||||||
{
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(c => c.Id == id)
|
|
||||||
.Include(e => e.Realm)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
|
|
||||||
|
|
||||||
if (HttpContext.Items["CurrentUser"] is Account currentUser)
|
|
||||||
chatRoom = await crs.LoadDirectMessageMembers(chatRoom, Guid.Parse(currentUser.Id));
|
|
||||||
|
|
||||||
return Ok(chatRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms()
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
|
||||||
return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var chatRooms = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.JoinedAt != null)
|
|
||||||
.Where(m => m.LeaveAt == null)
|
|
||||||
.Include(m => m.ChatRoom)
|
|
||||||
.Select(m => m.ChatRoom)
|
|
||||||
.ToListAsync();
|
|
||||||
chatRooms = await crs.LoadDirectMessageMembers(chatRooms, accountId);
|
|
||||||
chatRooms = await crs.SortChatRoomByLastMessage(chatRooms);
|
|
||||||
|
|
||||||
return Ok(chatRooms);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DirectMessageRequest
|
|
||||||
{
|
|
||||||
[Required] public Guid RelatedUserId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("direct")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
var relatedUser = await accounts.GetAccountAsync(
|
|
||||||
new GetAccountRequest { Id = request.RelatedUserId.ToString() }
|
|
||||||
);
|
|
||||||
if (relatedUser is null)
|
|
||||||
return BadRequest("Related user was not found");
|
|
||||||
|
|
||||||
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
|
||||||
{
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
RelatedId = request.RelatedUserId.ToString(),
|
|
||||||
Status = -100
|
|
||||||
});
|
|
||||||
if (hasBlocked?.Value ?? false)
|
|
||||||
return StatusCode(403, "You cannot create direct message with a user that blocked you.");
|
|
||||||
|
|
||||||
// Check if DM already exists between these users
|
|
||||||
var existingDm = await db.ChatRooms
|
|
||||||
.Include(c => c.Members)
|
|
||||||
.Where(c => c.Type == ChatRoomType.DirectMessage && c.Members.Count == 2)
|
|
||||||
.Where(c => c.Members.Any(m => m.AccountId == Guid.Parse(currentUser.Id)))
|
|
||||||
.Where(c => c.Members.Any(m => m.AccountId == request.RelatedUserId))
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (existingDm != null)
|
|
||||||
return BadRequest("You already have a DM with this user.");
|
|
||||||
|
|
||||||
// Create new DM chat room
|
|
||||||
var dmRoom = new ChatRoom
|
|
||||||
{
|
|
||||||
Type = ChatRoomType.DirectMessage,
|
|
||||||
IsPublic = false,
|
|
||||||
Members = new List<ChatMember>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
|
||||||
Role = ChatMemberRole.Owner,
|
|
||||||
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
AccountId = request.RelatedUserId,
|
|
||||||
Role = ChatMemberRole.Member,
|
|
||||||
JoinedAt = null, // Pending status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
db.ChatRooms.Add(dmRoom);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.create",
|
|
||||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(dmRoom.Id.ToString()) } },
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId);
|
|
||||||
invitedMember.ChatRoom = dmRoom;
|
|
||||||
await _SendInviteNotify(invitedMember, currentUser);
|
|
||||||
|
|
||||||
return Ok(dmRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("direct/{accountId:guid}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid accountId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
var room = await db.ChatRooms
|
|
||||||
.Include(c => c.Members)
|
|
||||||
.Where(c => c.Type == ChatRoomType.DirectMessage && c.Members.Count == 2)
|
|
||||||
.Where(c => c.Members.Any(m => m.AccountId == Guid.Parse(currentUser.Id)))
|
|
||||||
.Where(c => c.Members.Any(m => m.AccountId == accountId))
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (room is null) return NotFound();
|
|
||||||
|
|
||||||
return Ok(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ChatRoomRequest
|
|
||||||
{
|
|
||||||
[Required] [MaxLength(1024)] public string? Name { get; set; }
|
|
||||||
[MaxLength(4096)] public string? Description { get; set; }
|
|
||||||
[MaxLength(32)] public string? PictureId { get; set; }
|
|
||||||
[MaxLength(32)] public string? BackgroundId { get; set; }
|
|
||||||
public Guid? RealmId { get; set; }
|
|
||||||
public bool? IsCommunity { get; set; }
|
|
||||||
public bool? IsPublic { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize]
|
|
||||||
[RequiredPermission("global", "chat.create")]
|
|
||||||
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
|
||||||
if (request.Name is null) return BadRequest("You cannot create a chat room without a name.");
|
|
||||||
|
|
||||||
var chatRoom = new ChatRoom
|
|
||||||
{
|
|
||||||
Name = request.Name,
|
|
||||||
Description = request.Description ?? string.Empty,
|
|
||||||
IsCommunity = request.IsCommunity ?? false,
|
|
||||||
IsPublic = request.IsPublic ?? false,
|
|
||||||
Type = ChatRoomType.Group,
|
|
||||||
Members = new List<ChatMember>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Role = ChatMemberRole.Owner,
|
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.RealmId is not null)
|
|
||||||
{
|
|
||||||
if (!await rs.IsMemberWithRole(request.RealmId.Value, Guid.Parse(currentUser.Id),
|
|
||||||
RealmMemberRole.Moderator))
|
|
||||||
return StatusCode(403, "You need at least be a moderator to create chat linked to the realm.");
|
|
||||||
chatRoom.RealmId = request.RealmId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.PictureId is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
|
||||||
if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
|
||||||
chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
|
||||||
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = fileResponse.Id,
|
|
||||||
Usage = "chatroom.picture",
|
|
||||||
ResourceId = chatRoom.ResourceIdentifier,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.BackgroundId is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
|
||||||
if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
|
||||||
chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
|
||||||
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = fileResponse.Id,
|
|
||||||
Usage = "chatroom.background",
|
|
||||||
ResourceId = chatRoom.ResourceIdentifier,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return BadRequest("Invalid background id, unable to find the file on cloud.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db.ChatRooms.Add(chatRoom);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
|
|
||||||
|
|
||||||
if (chatRoom.Picture is not null)
|
|
||||||
{
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = chatRoom.Picture.Id,
|
|
||||||
Usage = "chat.room.picture",
|
|
||||||
ResourceId = chatRoomResourceId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chatRoom.Background is not null)
|
|
||||||
{
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = chatRoom.Background.Id,
|
|
||||||
Usage = "chat.room.background",
|
|
||||||
ResourceId = chatRoomResourceId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.create",
|
|
||||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } },
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(chatRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPatch("{id:guid}")]
|
|
||||||
public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(e => e.Id == id)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
|
|
||||||
if (chatRoom.RealmId is not null)
|
|
||||||
{
|
|
||||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
|
||||||
RealmMemberRole.Moderator))
|
|
||||||
return StatusCode(403, "You need at least be a realm moderator to update the chat.");
|
|
||||||
}
|
|
||||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
|
|
||||||
return StatusCode(403, "You need at least be a moderator to update the chat.");
|
|
||||||
|
|
||||||
if (request.RealmId is not null)
|
|
||||||
{
|
|
||||||
var member = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
|
||||||
.Where(m => m.RealmId == request.RealmId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null || member.Role < RealmMemberRole.Moderator)
|
|
||||||
return StatusCode(403, "You need at least be a moderator to transfer the chat linked to the realm.");
|
|
||||||
chatRoom.RealmId = member.RealmId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.PictureId is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
|
||||||
if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
|
||||||
|
|
||||||
// Remove old references for pictures
|
|
||||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
|
||||||
{
|
|
||||||
ResourceId = chatRoom.ResourceIdentifier,
|
|
||||||
Usage = "chat.room.picture"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a new reference
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = fileResponse.Id,
|
|
||||||
Usage = "chat.room.picture",
|
|
||||||
ResourceId = chatRoom.ResourceIdentifier
|
|
||||||
});
|
|
||||||
|
|
||||||
chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
|
||||||
}
|
|
||||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.BackgroundId is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
|
||||||
if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
|
||||||
|
|
||||||
// Remove old references for backgrounds
|
|
||||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
|
||||||
{
|
|
||||||
ResourceId = chatRoom.ResourceIdentifier,
|
|
||||||
Usage = "chat.room.background"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a new reference
|
|
||||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
|
||||||
{
|
|
||||||
FileId = fileResponse.Id,
|
|
||||||
Usage = "chat.room.background",
|
|
||||||
ResourceId = chatRoom.ResourceIdentifier
|
|
||||||
});
|
|
||||||
|
|
||||||
chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
|
||||||
}
|
|
||||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return BadRequest("Invalid background id, unable to find the file on cloud.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Name is not null)
|
|
||||||
chatRoom.Name = request.Name;
|
|
||||||
if (request.Description is not null)
|
|
||||||
chatRoom.Description = request.Description;
|
|
||||||
if (request.IsCommunity is not null)
|
|
||||||
chatRoom.IsCommunity = request.IsCommunity.Value;
|
|
||||||
if (request.IsPublic is not null)
|
|
||||||
chatRoom.IsPublic = request.IsPublic.Value;
|
|
||||||
|
|
||||||
db.ChatRooms.Update(chatRoom);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.update",
|
|
||||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } },
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(chatRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
|
||||||
public async Task<ActionResult> DeleteChatRoom(Guid id)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(e => e.Id == id)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
|
|
||||||
if (chatRoom.RealmId is not null)
|
|
||||||
{
|
|
||||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
|
||||||
RealmMemberRole.Moderator))
|
|
||||||
return StatusCode(403, "You need at least be a realm moderator to delete the chat.");
|
|
||||||
}
|
|
||||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Owner))
|
|
||||||
return StatusCode(403, "You need at least be the owner to delete the chat.");
|
|
||||||
|
|
||||||
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
|
|
||||||
|
|
||||||
// Delete all file references for this chat room
|
|
||||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
|
||||||
{
|
|
||||||
ResourceId = chatRoomResourceId
|
|
||||||
});
|
|
||||||
|
|
||||||
db.ChatRooms.Remove(chatRoom);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.delete",
|
|
||||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } },
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{roomId:guid}/members/me")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId)
|
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (member == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
return Ok(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{roomId:guid}/members")]
|
|
||||||
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20,
|
|
||||||
[FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null)
|
|
||||||
{
|
|
||||||
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Proto.Account;
|
|
||||||
|
|
||||||
var room = await db.ChatRooms
|
|
||||||
.FirstOrDefaultAsync(r => r.Id == roomId);
|
|
||||||
if (room is null) return NotFound();
|
|
||||||
|
|
||||||
if (!room.IsPublic)
|
|
||||||
{
|
|
||||||
if (currentUser is null) return Unauthorized();
|
|
||||||
var member = await db.ChatMembers
|
|
||||||
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id));
|
|
||||||
if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room.");
|
|
||||||
}
|
|
||||||
|
|
||||||
IQueryable<ChatMember> query = db.ChatMembers
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
|
||||||
.Where(m => m.LeaveAt == null) // Add this condition to exclude left members
|
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile);
|
|
||||||
|
|
||||||
// if (withStatus)
|
|
||||||
// {
|
|
||||||
// var members = await query
|
|
||||||
// .OrderBy(m => m.JoinedAt)
|
|
||||||
// .ToListAsync();
|
|
||||||
//
|
|
||||||
// var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList());
|
|
||||||
//
|
|
||||||
// if (!string.IsNullOrEmpty(status))
|
|
||||||
// {
|
|
||||||
// members = members.Where(m =>
|
|
||||||
// memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null &&
|
|
||||||
// s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline)
|
|
||||||
// .ToList();
|
|
||||||
//
|
|
||||||
// var total = members.Count;
|
|
||||||
// Response.Headers.Append("X-Total", total.ToString());
|
|
||||||
//
|
|
||||||
// var result = members.Skip(skip).Take(take).ToList();
|
|
||||||
//
|
|
||||||
// return Ok(result);
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
var total = await query.CountAsync();
|
|
||||||
Response.Headers.Append("X-Total", total.ToString());
|
|
||||||
|
|
||||||
var members = await query
|
|
||||||
.OrderBy(m => m.JoinedAt)
|
|
||||||
.Skip(skip)
|
|
||||||
.Take(take)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return Ok(members);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class ChatMemberRequest
|
|
||||||
{
|
|
||||||
[Required] public Guid RelatedUserId { get; set; }
|
|
||||||
[Required] public int Role { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("invites/{roomId:guid}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId,
|
|
||||||
[FromBody] ChatMemberRequest request)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
// Get related user account
|
|
||||||
var relatedUser = await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
|
||||||
if (relatedUser == null) return BadRequest("Related user was not found");
|
|
||||||
|
|
||||||
// Check if the user has blocked the current user
|
|
||||||
var relationship = await accounts.GetRelationshipAsync(new GetRelationshipRequest
|
|
||||||
{
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
RelatedId = relatedUser.Id,
|
|
||||||
Status = -100
|
|
||||||
});
|
|
||||||
|
|
||||||
if (relationship != null && relationship.Relationship.Status == -100)
|
|
||||||
return StatusCode(403, "You cannot invite a user that blocked you.");
|
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(p => p.Id == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
|
|
||||||
// Handle realm-owned chat rooms
|
|
||||||
if (chatRoom.RealmId is not null)
|
|
||||||
{
|
|
||||||
var realmMember = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.RealmId == chatRoom.RealmId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
|
|
||||||
return StatusCode(403, "You need at least be a realm moderator to invite members to this chat.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var chatMember = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatMember is null) return StatusCode(403, "You are not even a member of the targeted chat room.");
|
|
||||||
if (chatMember.Role < ChatMemberRole.Moderator)
|
|
||||||
return StatusCode(403,
|
|
||||||
"You need at least be a moderator to invite other members to this chat room.");
|
|
||||||
if (chatMember.Role < request.Role)
|
|
||||||
return StatusCode(403, "You cannot invite member with higher permission than yours.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasExistingMember = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == request.RelatedUserId)
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
|
||||||
.Where(m => m.LeaveAt == null)
|
|
||||||
.AnyAsync();
|
|
||||||
if (hasExistingMember)
|
|
||||||
return BadRequest("This user has been joined the chat cannot be invited again.");
|
|
||||||
|
|
||||||
var newMember = new ChatMember
|
|
||||||
{
|
|
||||||
AccountId = Guid.Parse(relatedUser.Id),
|
|
||||||
ChatRoomId = roomId,
|
|
||||||
Role = request.Role,
|
|
||||||
};
|
|
||||||
|
|
||||||
db.ChatMembers.Add(newMember);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
newMember.ChatRoom = chatRoom;
|
|
||||||
await _SendInviteNotify(newMember, currentUser);
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.invite",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) },
|
|
||||||
{ "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(relatedUser.Id.ToString()) }
|
|
||||||
},
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(newMember);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("invites")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<List<ChatMember>>> ListChatInvites()
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var members = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.JoinedAt == null)
|
|
||||||
.Include(e => e.ChatRoom)
|
|
||||||
.Include(e => e.Account)
|
|
||||||
.Include(e => e.Account.Profile)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var chatRooms = members.Select(m => m.ChatRoom).ToList();
|
|
||||||
var directMembers =
|
|
||||||
(await crs.LoadDirectMessageMembers(chatRooms, accountId)).ToDictionary(c => c.Id, c => c.Members);
|
|
||||||
|
|
||||||
foreach (var member in members.Where(member => member.ChatRoom.Type == ChatRoomType.DirectMessage))
|
|
||||||
member.ChatRoom.Members = directMembers[member.ChatRoom.Id];
|
|
||||||
|
|
||||||
return members.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("invites/{roomId:guid}/accept")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
|
||||||
.Where(m => m.JoinedAt == null)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
member.JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
|
||||||
db.Update(member);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
_ = crs.PurgeRoomMembersCache(roomId);
|
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
|
||||||
ActionLogType.ChatroomJoin,
|
|
||||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("invites/{roomId:guid}/decline")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult> DeclineChatInvite(Guid roomId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
|
||||||
.Where(m => m.JoinedAt == null)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ChatMemberNotifyRequest
|
|
||||||
{
|
|
||||||
public ChatMemberNotify? NotifyLevel { get; set; }
|
|
||||||
public Instant? BreakUntil { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("{roomId:guid}/members/me/notify")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatMember>> UpdateChatMemberNotify(
|
|
||||||
Guid roomId,
|
|
||||||
[FromBody] ChatMemberNotifyRequest request
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(r => r.Id == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
var targetMember = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (targetMember is null) return BadRequest("You have not joined this chat room.");
|
|
||||||
if (request.NotifyLevel is not null)
|
|
||||||
targetMember.Notify = request.NotifyLevel.Value;
|
|
||||||
if (request.BreakUntil is not null)
|
|
||||||
targetMember.BreakUntil = request.BreakUntil.Value;
|
|
||||||
|
|
||||||
db.ChatMembers.Update(targetMember);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
await crs.PurgeRoomMembersCache(roomId);
|
|
||||||
|
|
||||||
return Ok(targetMember);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("{roomId:guid}/members/{memberId:guid}/role")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
|
|
||||||
{
|
|
||||||
if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role.");
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(r => r.Id == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
|
|
||||||
// Check if the chat room is owned by a realm
|
|
||||||
if (chatRoom.RealmId is not null)
|
|
||||||
{
|
|
||||||
var realmMember = await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
|
||||||
.Where(m => m.RealmId == chatRoom.RealmId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
|
|
||||||
return StatusCode(403, "You need at least be a realm moderator to change member roles.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var targetMember = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == memberId && m.ChatRoomId == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (targetMember is null) return NotFound();
|
|
||||||
|
|
||||||
// Check if the current user has permission to change roles
|
|
||||||
if (
|
|
||||||
!await crs.IsMemberWithRole(
|
|
||||||
chatRoom.Id,
|
|
||||||
Guid.Parse(currentUser.Id),
|
|
||||||
ChatMemberRole.Moderator,
|
|
||||||
targetMember.Role,
|
|
||||||
newRole
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return StatusCode(403, "You don't have enough permission to edit the roles of members.");
|
|
||||||
|
|
||||||
targetMember.Role = newRole;
|
|
||||||
db.ChatMembers.Update(targetMember);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
await crs.PurgeRoomMembersCache(roomId);
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.role.edit",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) },
|
|
||||||
{ "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(memberId.ToString()) },
|
|
||||||
{ "new_role", Google.Protobuf.WellKnownTypes.Value.ForNumber(newRole) }
|
|
||||||
},
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(targetMember);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{roomId:guid}/members/{memberId:guid}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult> RemoveChatMember(Guid roomId, Guid memberId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(r => r.Id == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
|
|
||||||
// Check if the chat room is owned by a realm
|
|
||||||
if (chatRoom.RealmId is not null)
|
|
||||||
{
|
|
||||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
|
||||||
RealmMemberRole.Moderator))
|
|
||||||
return StatusCode(403, "You need at least be a realm moderator to remove members.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
|
|
||||||
return StatusCode(403, "You need at least be a moderator to remove members.");
|
|
||||||
|
|
||||||
// Find the target member
|
|
||||||
var member = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == memberId && m.ChatRoomId == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
// Check if the current user has sufficient permissions
|
|
||||||
if (!await crs.IsMemberWithRole(chatRoom.Id, memberId, member.Role))
|
|
||||||
return StatusCode(403, "You cannot remove members with equal or higher roles.");
|
|
||||||
|
|
||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
_ = crs.PurgeRoomMembersCache(roomId);
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.kick",
|
|
||||||
Meta =
|
|
||||||
{
|
|
||||||
{ "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) },
|
|
||||||
{ "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(memberId.ToString()) }
|
|
||||||
},
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("{roomId:guid}/members/me")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
|
||||||
.Where(r => r.Id == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (chatRoom is null) return NotFound();
|
|
||||||
if (!chatRoom.IsCommunity)
|
|
||||||
return StatusCode(403, "This chat room isn't a community. You need an invitation to join.");
|
|
||||||
|
|
||||||
var existingMember = await db.ChatMembers
|
|
||||||
.FirstOrDefaultAsync(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId);
|
|
||||||
if (existingMember != null)
|
|
||||||
return BadRequest("You are already a member of this chat room.");
|
|
||||||
|
|
||||||
var newMember = new ChatMember
|
|
||||||
{
|
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
|
||||||
ChatRoomId = roomId,
|
|
||||||
Role = ChatMemberRole.Member,
|
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
|
||||||
};
|
|
||||||
|
|
||||||
db.ChatMembers.Add(newMember);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
_ = crs.PurgeRoomMembersCache(roomId);
|
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
|
||||||
ActionLogType.ChatroomJoin,
|
|
||||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(chatRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{roomId:guid}/members/me")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult> LeaveChat(Guid roomId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null) return NotFound();
|
|
||||||
|
|
||||||
if (member.Role == ChatMemberRole.Owner)
|
|
||||||
{
|
|
||||||
// Check if this is the only owner
|
|
||||||
var otherOwners = await db.ChatMembers
|
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
|
||||||
.Where(m => m.Role == ChatMemberRole.Owner)
|
|
||||||
.Where(m => m.AccountId != Guid.Parse(currentUser.Id))
|
|
||||||
.AnyAsync();
|
|
||||||
|
|
||||||
if (!otherOwners)
|
|
||||||
return BadRequest("The last owner cannot leave the chat. Transfer ownership first or delete the chat.");
|
|
||||||
}
|
|
||||||
|
|
||||||
member.LeaveAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
await crs.PurgeRoomMembersCache(roomId);
|
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
|
||||||
{
|
|
||||||
Action = "chatrooms.leave",
|
|
||||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) } },
|
|
||||||
AccountId = currentUser.Id,
|
|
||||||
UserAgent = Request.Headers.UserAgent,
|
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task _SendInviteNotify(ChatMember member, Account sender)
|
|
||||||
{
|
|
||||||
string title = localizer["ChatInviteTitle"];
|
|
||||||
|
|
||||||
string body = member.ChatRoom.Type == ChatRoomType.DirectMessage
|
|
||||||
? localizer["ChatInviteDirectBody", sender.Nick]
|
|
||||||
: localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"];
|
|
||||||
|
|
||||||
AccountService.SetCultureInfo(member.Account);
|
|
||||||
await nty.SendNotification(member.Account, "invites.chats", title, null, body, actionUri: "/chat");
|
|
||||||
}
|
|
||||||
}
|
|
@@ -233,6 +233,7 @@ public partial class ChatService(
|
|||||||
Body = !string.IsNullOrEmpty(message.Content)
|
Body = !string.IsNullOrEmpty(message.Content)
|
||||||
? message.Content[..Math.Min(message.Content.Length, 100)]
|
? message.Content[..Math.Min(message.Content.Length, 100)]
|
||||||
: "<no content>",
|
: "<no content>",
|
||||||
|
IsSavable = false
|
||||||
};
|
};
|
||||||
notification.Meta.Add(GrpcTypeHelper.ConvertToValueMap(metaDict));
|
notification.Meta.Add(GrpcTypeHelper.ConvertToValueMap(metaDict));
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Content;
|
using DysonNetwork.Shared.Content;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Sphere;
|
using DysonNetwork.Sphere;
|
||||||
using DysonNetwork.Sphere.Startup;
|
using DysonNetwork.Sphere.Startup;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -12,13 +14,15 @@ builder.ConfigureAppKestrel();
|
|||||||
builder.Services.AddAppMetrics();
|
builder.Services.AddAppMetrics();
|
||||||
|
|
||||||
// Add application services
|
// Add application services
|
||||||
|
builder.Services.AddRegistryService(builder.Configuration);
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
builder.Services.AddAppServices(builder.Configuration);
|
||||||
builder.Services.AddAppRateLimiting();
|
builder.Services.AddAppRateLimiting();
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddAppSwagger();
|
builder.Services.AddAppSwagger();
|
||||||
|
builder.Services.AddDysonAuth();
|
||||||
// Add file storage
|
builder.Services.AddAccountService();
|
||||||
builder.Services.AddAppFileStorage(builder.Configuration);
|
builder.Services.AddPusherService();
|
||||||
|
builder.Services.AddDriveService();
|
||||||
|
|
||||||
// Add flush handlers and websocket handlers
|
// Add flush handlers and websocket handlers
|
||||||
builder.Services.AddAppFlushHandlers();
|
builder.Services.AddAppFlushHandlers();
|
||||||
@@ -38,10 +42,7 @@ using (var scope = app.Services.CreateScope())
|
|||||||
await db.Database.MigrateAsync();
|
await db.Database.MigrateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the TusDiskStore instance
|
|
||||||
var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
|
|
||||||
|
|
||||||
// Configure application middleware pipeline
|
// Configure application middleware pipeline
|
||||||
app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore);
|
app.ConfigureAppMiddleware(builder.Configuration);
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
@@ -1,7 +1,8 @@
|
|||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Shared;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
||||||
@@ -9,10 +10,11 @@ namespace DysonNetwork.Sphere.Publisher;
|
|||||||
|
|
||||||
public class PublisherSubscriptionService(
|
public class PublisherSubscriptionService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
NotificationService nty,
|
|
||||||
PostService ps,
|
PostService ps,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
ICacheService cache
|
ICacheService cache,
|
||||||
|
PusherService.PusherServiceClient pusher,
|
||||||
|
AccountService.AccountServiceClient accounts
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -50,7 +52,6 @@ public class PublisherSubscriptionService(
|
|||||||
public async Task<int> NotifySubscriberPost(Post.Post post)
|
public async Task<int> NotifySubscriberPost(Post.Post post)
|
||||||
{
|
{
|
||||||
var subscribers = await db.PublisherSubscriptions
|
var subscribers = await db.PublisherSubscriptions
|
||||||
.Include(p => p.Account)
|
|
||||||
.Where(p => p.PublisherId == post.PublisherId &&
|
.Where(p => p.PublisherId == post.PublisherId &&
|
||||||
p.Status == PublisherSubscriptionStatus.Active)
|
p.Status == PublisherSubscriptionStatus.Active)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -67,23 +68,35 @@ public class PublisherSubscriptionService(
|
|||||||
{ "publisher_id", post.Publisher.Id.ToString() }
|
{ "publisher_id", post.Publisher.Id.ToString() }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var queryRequest = new GetAccountBatchRequest();
|
||||||
|
queryRequest.Id.AddRange(subscribers.DistinctBy(s => s.AccountId).Select(m => m.AccountId.ToString()));
|
||||||
|
var queryResponse = await accounts.GetAccountBatchAsync(queryRequest);
|
||||||
|
|
||||||
|
var notification = new PushNotification
|
||||||
|
{
|
||||||
|
Topic = "posts.new",
|
||||||
|
Title = localizer["PostSubscriptionTitle", post.Publisher.Name, title],
|
||||||
|
Body = message,
|
||||||
|
IsSavable = true,
|
||||||
|
ActionUri = $"/posts/{post.Id}"
|
||||||
|
};
|
||||||
|
notification.Meta.Add(GrpcTypeHelper.ConvertToValueMap(data));
|
||||||
|
|
||||||
// Notify each subscriber
|
// Notify each subscriber
|
||||||
var notifiedCount = 0;
|
var notifiedCount = 0;
|
||||||
foreach (var subscription in subscribers.DistinctBy(s => s.AccountId))
|
foreach (var target in queryResponse.Accounts)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AccountService.SetCultureInfo(subscription.Account);
|
CultureService.SetCultureInfo(target);
|
||||||
await nty.SendNotification(
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
subscription.Account,
|
new SendPushNotificationToUserRequest
|
||||||
"posts.new",
|
{
|
||||||
localizer["PostSubscriptionTitle", post.Publisher.Name, title],
|
UserId = target.Id,
|
||||||
null,
|
Notification = notification
|
||||||
message,
|
}
|
||||||
data,
|
|
||||||
actionUri: $"/posts/{post.Id}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
notifiedCount++;
|
notifiedCount++;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -117,7 +130,6 @@ public class PublisherSubscriptionService(
|
|||||||
public async Task<List<PublisherSubscription>> GetPublisherSubscribersAsync(Guid publisherId)
|
public async Task<List<PublisherSubscription>> GetPublisherSubscribersAsync(Guid publisherId)
|
||||||
{
|
{
|
||||||
return await db.PublisherSubscriptions
|
return await db.PublisherSubscriptions
|
||||||
.Include(ps => ps.Account)
|
|
||||||
.Where(ps => ps.PublisherId == publisherId && ps.Status == PublisherSubscriptionStatus.Active)
|
.Where(ps => ps.PublisherId == publisherId && ps.Status == PublisherSubscriptionStatus.Active)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ public class Realm : ModelBase, IIdentifiedResource
|
|||||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||||
|
|
||||||
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; }
|
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
|
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
|
||||||
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
|
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
|
||||||
@@ -48,7 +48,6 @@ public class RealmMember : ModelBase
|
|||||||
public Guid RealmId { get; set; }
|
public Guid RealmId { get; set; }
|
||||||
public Realm Realm { get; set; } = null!;
|
public Realm Realm { get; set; } = null!;
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public Account Account { get; set; } = null!;
|
|
||||||
|
|
||||||
public int Role { get; set; } = RealmMemberRole.Normal;
|
public int Role { get; set; } = RealmMemberRole.Normal;
|
||||||
public Instant? JoinedAt { get; set; }
|
public Instant? JoinedAt { get; set; }
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -14,6 +15,7 @@ public class RealmChatController(AppDatabase db, RealmService rs) : ControllerBa
|
|||||||
public async Task<ActionResult<List<ChatRoom>>> ListRealmChat(string slug)
|
public async Task<ActionResult<List<ChatRoom>>> ListRealmChat(string slug)
|
||||||
{
|
{
|
||||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||||
|
var accountId = currentUser is null ? Guid.Empty : Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
.Where(r => r.Slug == slug)
|
.Where(r => r.Slug == slug)
|
||||||
@@ -22,7 +24,7 @@ public class RealmChatController(AppDatabase db, RealmService rs) : ControllerBa
|
|||||||
if (!realm.IsPublic)
|
if (!realm.IsPublic)
|
||||||
{
|
{
|
||||||
if (currentUser is null) return Unauthorized();
|
if (currentUser is null) return Unauthorized();
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Normal))
|
if (!await rs.IsMemberWithRole(realm.Id, accountId, RealmMemberRole.Normal))
|
||||||
return StatusCode(403, "You need at least one member to view the realm's chat.");
|
return StatusCode(403, "You need at least one member to view the realm's chat.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using DysonNetwork.Shared.Data;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Realm;
|
namespace DysonNetwork.Sphere.Realm;
|
||||||
|
|
||||||
@@ -11,10 +14,10 @@ namespace DysonNetwork.Sphere.Realm;
|
|||||||
public class RealmController(
|
public class RealmController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
RealmService rs,
|
RealmService rs,
|
||||||
FileReferenceService fileRefService,
|
FileService.FileServiceClient files,
|
||||||
RelationshipService rels,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
ActionLogService als,
|
ActionLogService.ActionLogServiceClient als,
|
||||||
AccountEventService aes
|
AccountService.AccountServiceClient accounts
|
||||||
) : Controller
|
) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("{slug}")]
|
[HttpGet("{slug}")]
|
||||||
@@ -33,10 +36,10 @@ public class RealmController(
|
|||||||
public async Task<ActionResult<List<Realm>>> ListJoinedRealms()
|
public async Task<ActionResult<List<Realm>>> ListJoinedRealms()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var members = await db.RealmMembers
|
var members = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == userId)
|
.Where(m => m.AccountId == accountId)
|
||||||
.Where(m => m.JoinedAt != null)
|
.Where(m => m.JoinedAt != null)
|
||||||
.Where(m => m.LeaveAt == null)
|
.Where(m => m.LeaveAt == null)
|
||||||
.Include(e => e.Realm)
|
.Include(e => e.Realm)
|
||||||
@@ -51,10 +54,10 @@ public class RealmController(
|
|||||||
public async Task<ActionResult<List<RealmMember>>> ListInvites()
|
public async Task<ActionResult<List<RealmMember>>> ListInvites()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var members = await db.RealmMembers
|
var members = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == userId)
|
.Where(m => m.AccountId == accountId)
|
||||||
.Where(m => m.JoinedAt == null)
|
.Where(m => m.JoinedAt == null)
|
||||||
.Include(e => e.Realm)
|
.Include(e => e.Realm)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -74,12 +77,19 @@ public class RealmController(
|
|||||||
[FromBody] RealmMemberRequest request)
|
[FromBody] RealmMemberRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
var relatedUser =
|
||||||
if (relatedUser is null) return BadRequest("Related user was not found");
|
await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
||||||
|
if (relatedUser == null) return BadRequest("Related user was not found");
|
||||||
|
|
||||||
if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked))
|
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
||||||
|
{
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
RelatedId = request.RelatedUserId.ToString(),
|
||||||
|
Status = -100
|
||||||
|
});
|
||||||
|
if (hasBlocked?.Value ?? false)
|
||||||
return StatusCode(403, "You cannot invite a user that blocked you.");
|
return StatusCode(403, "You cannot invite a user that blocked you.");
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
@@ -87,11 +97,11 @@ public class RealmController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (realm is null) return NotFound();
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, userId, request.Role))
|
if (!await rs.IsMemberWithRole(realm.Id, accountId, request.Role))
|
||||||
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
||||||
|
|
||||||
var hasExistingMember = await db.RealmMembers
|
var hasExistingMember = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == request.RelatedUserId)
|
.Where(m => m.AccountId == Guid.Parse(relatedUser.Id))
|
||||||
.Where(m => m.RealmId == realm.Id)
|
.Where(m => m.RealmId == realm.Id)
|
||||||
.Where(m => m.LeaveAt == null)
|
.Where(m => m.LeaveAt == null)
|
||||||
.AnyAsync();
|
.AnyAsync();
|
||||||
@@ -100,7 +110,7 @@ public class RealmController(
|
|||||||
|
|
||||||
var member = new RealmMember
|
var member = new RealmMember
|
||||||
{
|
{
|
||||||
AccountId = relatedUser.Id,
|
AccountId = Guid.Parse(relatedUser.Id),
|
||||||
RealmId = realm.Id,
|
RealmId = realm.Id,
|
||||||
Role = request.Role,
|
Role = request.Role,
|
||||||
};
|
};
|
||||||
@@ -108,12 +118,21 @@ public class RealmController(
|
|||||||
db.RealmMembers.Add(member);
|
db.RealmMembers.Add(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmInvite,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", member.AccountId } }, Request
|
Action = "realms.members.invite",
|
||||||
);
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||||
|
{ "role", Value.ForNumber(request.Role) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
member.Account = relatedUser;
|
member.AccountId = Guid.Parse(relatedUser.Id);
|
||||||
member.Realm = realm;
|
member.Realm = realm;
|
||||||
await rs.SendInviteNotify(member);
|
await rs.SendInviteNotify(member);
|
||||||
|
|
||||||
@@ -125,10 +144,10 @@ public class RealmController(
|
|||||||
public async Task<ActionResult<Realm>> AcceptMemberInvite(string slug)
|
public async Task<ActionResult<Realm>> AcceptMemberInvite(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == userId)
|
.Where(m => m.AccountId == accountId)
|
||||||
.Where(m => m.Realm.Slug == slug)
|
.Where(m => m.Realm.Slug == slug)
|
||||||
.Where(m => m.JoinedAt == null)
|
.Where(m => m.JoinedAt == null)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
@@ -138,11 +157,18 @@ public class RealmController(
|
|||||||
db.Update(member);
|
db.Update(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmJoin,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
Action = "realms.members.join",
|
||||||
Request
|
Meta =
|
||||||
);
|
{
|
||||||
|
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(member.AccountId.ToString()) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
return Ok(member);
|
return Ok(member);
|
||||||
}
|
}
|
||||||
@@ -152,10 +178,10 @@ public class RealmController(
|
|||||||
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == userId)
|
.Where(m => m.AccountId == accountId)
|
||||||
.Where(m => m.Realm.Slug == slug)
|
.Where(m => m.Realm.Slug == slug)
|
||||||
.Where(m => m.JoinedAt == null)
|
.Where(m => m.JoinedAt == null)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
@@ -164,11 +190,19 @@ public class RealmController(
|
|||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmLeave,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
Action = "realms.members.decline_invite",
|
||||||
Request
|
Meta =
|
||||||
);
|
{
|
||||||
|
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||||
|
{ "decliner_id", Value.ForString(currentUser.Id) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -191,43 +225,41 @@ public class RealmController(
|
|||||||
if (!realm.IsPublic)
|
if (!realm.IsPublic)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Normal))
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal))
|
||||||
return StatusCode(403, "You must be a member to view this realm's members.");
|
return StatusCode(403, "You must be a member to view this realm's members.");
|
||||||
}
|
}
|
||||||
|
|
||||||
IQueryable<RealmMember> query = db.RealmMembers
|
var query = db.RealmMembers
|
||||||
.Where(m => m.RealmId == realm.Id)
|
.Where(m => m.RealmId == realm.Id)
|
||||||
.Where(m => m.LeaveAt == null)
|
.Where(m => m.LeaveAt == null);
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile);
|
|
||||||
|
|
||||||
if (withStatus)
|
// if (withStatus)
|
||||||
{
|
// {
|
||||||
var members = await query
|
// var members = await query
|
||||||
.OrderBy(m => m.CreatedAt)
|
// .OrderBy(m => m.CreatedAt)
|
||||||
.ToListAsync();
|
// .ToListAsync();
|
||||||
|
//
|
||||||
var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList());
|
// var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList());
|
||||||
|
//
|
||||||
if (!string.IsNullOrEmpty(status))
|
// if (!string.IsNullOrEmpty(status))
|
||||||
{
|
// {
|
||||||
members = members.Where(m =>
|
// members = members.Where(m =>
|
||||||
memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null &&
|
// memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null &&
|
||||||
s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList();
|
// s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline)
|
// members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline)
|
||||||
.ToList();
|
// .ToList();
|
||||||
|
//
|
||||||
var total = members.Count;
|
// var total = members.Count;
|
||||||
Response.Headers["X-Total"] = total.ToString();
|
// Response.Headers["X-Total"] = total.ToString();
|
||||||
|
//
|
||||||
var result = members.Skip(offset).Take(take).ToList();
|
// var result = members.Skip(offset).Take(take).ToList();
|
||||||
|
//
|
||||||
return Ok(result);
|
// return Ok(result);
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
var total = await query.CountAsync();
|
var total = await query.CountAsync();
|
||||||
Response.Headers["X-Total"] = total.ToString();
|
Response.Headers["X-Total"] = total.ToString();
|
||||||
|
|
||||||
@@ -238,23 +270,20 @@ public class RealmController(
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return Ok(members);
|
return Ok(members);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("{slug}/members/me")]
|
[HttpGet("{slug}/members/me")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<RealmMember>> GetCurrentIdentity(string slug)
|
public async Task<ActionResult<RealmMember>> GetCurrentIdentity(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == userId)
|
.Where(m => m.AccountId == accountId)
|
||||||
.Where(m => m.Realm.Slug == slug)
|
.Where(m => m.Realm.Slug == slug)
|
||||||
.Include(m => m.Account)
|
|
||||||
.Include(m => m.Account.Profile)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (member is null) return NotFound();
|
if (member is null) return NotFound();
|
||||||
@@ -266,10 +295,10 @@ public class RealmController(
|
|||||||
public async Task<ActionResult> LeaveRealm(string slug)
|
public async Task<ActionResult> LeaveRealm(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == userId)
|
.Where(m => m.AccountId == accountId)
|
||||||
.Where(m => m.Realm.Slug == slug)
|
.Where(m => m.Realm.Slug == slug)
|
||||||
.Where(m => m.JoinedAt != null)
|
.Where(m => m.JoinedAt != null)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
@@ -281,11 +310,19 @@ public class RealmController(
|
|||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmLeave,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
Action = "realms.members.leave",
|
||||||
Request
|
Meta =
|
||||||
);
|
{
|
||||||
|
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||||
|
{ "leaver_id", Value.ForString(currentUser.Id) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -317,7 +354,7 @@ public class RealmController(
|
|||||||
Name = request.Name!,
|
Name = request.Name!,
|
||||||
Slug = request.Slug!,
|
Slug = request.Slug!,
|
||||||
Description = request.Description!,
|
Description = request.Description!,
|
||||||
AccountId = currentUser.Id,
|
AccountId = Guid.Parse(currentUser.Id),
|
||||||
IsCommunity = request.IsCommunity ?? false,
|
IsCommunity = request.IsCommunity ?? false,
|
||||||
IsPublic = request.IsPublic ?? false,
|
IsPublic = request.IsPublic ?? false,
|
||||||
Members = new List<RealmMember>
|
Members = new List<RealmMember>
|
||||||
@@ -325,7 +362,7 @@ public class RealmController(
|
|||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Role = RealmMemberRole.Owner,
|
Role = RealmMemberRole.Owner,
|
||||||
AccountId = currentUser.Id,
|
AccountId = Guid.Parse(currentUser.Id),
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,42 +370,56 @@ public class RealmController(
|
|||||||
|
|
||||||
if (request.PictureId is not null)
|
if (request.PictureId is not null)
|
||||||
{
|
{
|
||||||
realm.Picture = (await db.Files.FindAsync(request.PictureId))?.ToReferenceObject();
|
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||||
if (realm.Picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.BackgroundId is not null)
|
if (request.BackgroundId is not null)
|
||||||
{
|
{
|
||||||
realm.Background = (await db.Files.FindAsync(request.BackgroundId))?.ToReferenceObject();
|
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||||
if (realm.Background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||||
|
realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Realms.Add(realm);
|
db.Realms.Add(realm);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmCreate,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
Action = "realms.create",
|
||||||
);
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "name", Value.ForString(realm.Name) },
|
||||||
|
{ "slug", Value.ForString(realm.Slug) },
|
||||||
|
{ "is_community", Value.ForBool(realm.IsCommunity) },
|
||||||
|
{ "is_public", Value.ForBool(realm.IsPublic) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
var realmResourceId = $"realm:{realm.Id}";
|
var realmResourceId = $"realm:{realm.Id}";
|
||||||
|
|
||||||
if (realm.Picture is not null)
|
if (realm.Picture is not null)
|
||||||
{
|
{
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
realm.Picture.Id,
|
{
|
||||||
"realm.picture",
|
FileId = realm.Picture.Id,
|
||||||
realmResourceId
|
Usage = "realm.picture",
|
||||||
);
|
ResourceId = realmResourceId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (realm.Background is not null)
|
if (realm.Background is not null)
|
||||||
{
|
{
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
realm.Background.Id,
|
{
|
||||||
"realm.background",
|
FileId = realm.Background.Id,
|
||||||
realmResourceId
|
Usage = "realm.background",
|
||||||
);
|
ResourceId = realmResourceId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(realm);
|
return Ok(realm);
|
||||||
@@ -385,8 +436,9 @@ public class RealmController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (realm is null) return NotFound();
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id && m.JoinedAt != null)
|
.Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member is null || member.Role < RealmMemberRole.Moderator)
|
if (member is null || member.Role < RealmMemberRole.Moderator)
|
||||||
return StatusCode(403, "You do not have permission to update this realm.");
|
return StatusCode(403, "You do not have permission to update this realm.");
|
||||||
@@ -409,53 +461,75 @@ public class RealmController(
|
|||||||
|
|
||||||
if (request.PictureId is not null)
|
if (request.PictureId is not null)
|
||||||
{
|
{
|
||||||
var picture = await db.Files.FindAsync(request.PictureId);
|
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||||
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
|
||||||
// Remove old references for the realm picture
|
// Remove old references for the realm picture
|
||||||
if (realm.Picture is not null)
|
if (realm.Picture is not null)
|
||||||
{
|
{
|
||||||
await fileRefService.DeleteResourceReferencesAsync(realm.ResourceIdentifier, "realm.picture");
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||||
|
{
|
||||||
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
realm.Picture = picture.ToReferenceObject();
|
realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult);
|
||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
picture.Id,
|
{
|
||||||
"realm.picture",
|
FileId = realm.Picture.Id,
|
||||||
realm.ResourceIdentifier
|
Usage = "realm.picture",
|
||||||
);
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.BackgroundId is not null)
|
if (request.BackgroundId is not null)
|
||||||
{
|
{
|
||||||
var background = await db.Files.FindAsync(request.BackgroundId);
|
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||||
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||||
|
|
||||||
// Remove old references for the realm background
|
// Remove old references for the realm background
|
||||||
if (realm.Background is not null)
|
if (realm.Background is not null)
|
||||||
{
|
{
|
||||||
await fileRefService.DeleteResourceReferencesAsync(realm.ResourceIdentifier, "realm.background");
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||||
|
{
|
||||||
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
realm.Background = background.ToReferenceObject();
|
realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult);
|
||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
background.Id,
|
{
|
||||||
"realm.background",
|
FileId = realm.Background.Id,
|
||||||
realm.ResourceIdentifier
|
Usage = "realm.background",
|
||||||
);
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Realms.Update(realm);
|
db.Realms.Update(realm);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmUpdate,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
Action = "realms.update",
|
||||||
);
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "name_updated", Value.ForBool(request.Name != null) },
|
||||||
|
{ "slug_updated", Value.ForBool(request.Slug != null) },
|
||||||
|
{ "description_updated", Value.ForBool(request.Description != null) },
|
||||||
|
{ "picture_updated", Value.ForBool(request.PictureId != null) },
|
||||||
|
{ "background_updated", Value.ForBool(request.BackgroundId != null) },
|
||||||
|
{ "is_community_updated", Value.ForBool(request.IsCommunity != null) },
|
||||||
|
{ "is_public_updated", Value.ForBool(request.IsPublic != null) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
return Ok(realm);
|
return Ok(realm);
|
||||||
}
|
}
|
||||||
@@ -475,14 +549,14 @@ public class RealmController(
|
|||||||
return StatusCode(403, "Only community realms can be joined without invitation.");
|
return StatusCode(403, "Only community realms can be joined without invitation.");
|
||||||
|
|
||||||
var existingMember = await db.RealmMembers
|
var existingMember = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id)
|
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (existingMember is not null)
|
if (existingMember is not null)
|
||||||
return BadRequest("You are already a member of this realm.");
|
return BadRequest("You are already a member of this realm.");
|
||||||
|
|
||||||
var member = new RealmMember
|
var member = new RealmMember
|
||||||
{
|
{
|
||||||
AccountId = currentUser.Id,
|
AccountId = Guid.Parse(currentUser.Id),
|
||||||
RealmId = realm.Id,
|
RealmId = realm.Id,
|
||||||
Role = RealmMemberRole.Normal,
|
Role = RealmMemberRole.Normal,
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||||
@@ -491,11 +565,19 @@ public class RealmController(
|
|||||||
db.RealmMembers.Add(member);
|
db.RealmMembers.Add(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmJoin,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", currentUser.Id } },
|
Action = "realms.members.join",
|
||||||
Request
|
Meta =
|
||||||
);
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(currentUser.Id) },
|
||||||
|
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
return Ok(member);
|
return Ok(member);
|
||||||
}
|
}
|
||||||
@@ -516,17 +598,25 @@ public class RealmController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member is null) return NotFound();
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role))
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role))
|
||||||
return StatusCode(403, "You do not have permission to remove members from this realm.");
|
return StatusCode(403, "You do not have permission to remove members from this realm.");
|
||||||
|
|
||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.ChatroomKick,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", memberId } },
|
Action = "realms.members.kick",
|
||||||
Request
|
Meta =
|
||||||
);
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||||
|
{ "kicker_id", Value.ForString(currentUser.Id) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -545,23 +635,31 @@ public class RealmController(
|
|||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
|
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
|
||||||
.Include(m => m.Account)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member is null) return NotFound();
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role, newRole))
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role,
|
||||||
|
newRole))
|
||||||
return StatusCode(403, "You do not have permission to update member roles in this realm.");
|
return StatusCode(403, "You do not have permission to update member roles in this realm.");
|
||||||
|
|
||||||
member.Role = newRole;
|
member.Role = newRole;
|
||||||
db.RealmMembers.Update(member);
|
db.RealmMembers.Update(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmAdjustRole,
|
{
|
||||||
new Dictionary<string, object>
|
Action = "realms.members.role_update",
|
||||||
{ { "realm_id", realm.Id }, { "account_id", memberId }, { "new_role", newRole } },
|
Meta =
|
||||||
Request
|
{
|
||||||
);
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||||
|
{ "new_role", Value.ForNumber(newRole) },
|
||||||
|
{ "updater_id", Value.ForString(currentUser.Id) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
return Ok(member);
|
return Ok(member);
|
||||||
}
|
}
|
||||||
@@ -574,25 +672,35 @@ public class RealmController(
|
|||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
.Where(r => r.Slug == slug)
|
.Where(r => r.Slug == slug)
|
||||||
.Include(r => r.Picture)
|
|
||||||
.Include(r => r.Background)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (realm is null) return NotFound();
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Owner))
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner))
|
||||||
return StatusCode(403, "Only the owner can delete this realm.");
|
return StatusCode(403, "Only the owner can delete this realm.");
|
||||||
|
|
||||||
db.Realms.Remove(realm);
|
db.Realms.Remove(realm);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
ActionLogType.RealmDelete,
|
{
|
||||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
Action = "realms.delete",
|
||||||
);
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "realm_name", Value.ForString(realm.Name) },
|
||||||
|
{ "realm_slug", Value.ForString(realm.Slug) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
// Delete all file references for this realm
|
// Delete all file references for this realm
|
||||||
var realmResourceId = $"realm:{realm.Id}";
|
var realmResourceId = $"realm:{realm.Id}";
|
||||||
await fileRefService.DeleteResourceReferencesAsync(realmResourceId);
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||||
|
{
|
||||||
|
ResourceId = realmResourceId
|
||||||
|
});
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
696
DysonNetwork.Sphere/Realm/RealmController.cs.bak
Normal file
696
DysonNetwork.Sphere/Realm/RealmController.cs.bak
Normal file
@@ -0,0 +1,696 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Realm;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/realms")]
|
||||||
|
public class RealmController(
|
||||||
|
AppDatabase db,
|
||||||
|
RealmService rs,
|
||||||
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
|
ActionLogService.ActionLogServiceClient als,
|
||||||
|
AccountService.AccountServiceClient accounts
|
||||||
|
) : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("{slug}")]
|
||||||
|
public async Task<ActionResult<Realm>> GetRealm(string slug)
|
||||||
|
{
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(e => e.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
return Ok(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<Realm>>> ListJoinedRealms()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
|
var members = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == accountId)
|
||||||
|
.Where(m => m.JoinedAt != null)
|
||||||
|
.Where(m => m.LeaveAt == null)
|
||||||
|
.Include(e => e.Realm)
|
||||||
|
.Select(m => m.Realm)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return members.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("invites")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<RealmMember>>> ListInvites()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
|
var members = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == accountId)
|
||||||
|
.Where(m => m.JoinedAt == null)
|
||||||
|
.Include(e => e.Realm)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return members.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RealmMemberRequest
|
||||||
|
{
|
||||||
|
[Required] public Guid RelatedUserId { get; set; }
|
||||||
|
[Required] public int Role { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("invites/{slug}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<RealmMember>> InviteMember(string slug,
|
||||||
|
[FromBody] RealmMemberRequest request)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
|
var relatedUser =
|
||||||
|
await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
||||||
|
if (relatedUser == null) return BadRequest("Related user was not found");
|
||||||
|
|
||||||
|
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
||||||
|
{
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
RelatedId = request.RelatedUserId.ToString(),
|
||||||
|
Status = -100
|
||||||
|
});
|
||||||
|
if (hasBlocked?.Value ?? false)
|
||||||
|
return StatusCode(403, "You cannot invite a user that blocked you.");
|
||||||
|
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(p => p.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
if (!await rs.IsMemberWithRole(realm.Id, accountId, request.Role))
|
||||||
|
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
||||||
|
|
||||||
|
var hasExistingMember = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == Guid.Parse(relatedUser.Id))
|
||||||
|
.Where(m => m.RealmId == realm.Id)
|
||||||
|
.Where(m => m.LeaveAt == null)
|
||||||
|
.AnyAsync();
|
||||||
|
if (hasExistingMember)
|
||||||
|
return BadRequest("This user has been joined the realm or leave cannot be invited again.");
|
||||||
|
|
||||||
|
var member = new RealmMember
|
||||||
|
{
|
||||||
|
AccountId = Guid.Parse(relatedUser.Id),
|
||||||
|
RealmId = realm.Id,
|
||||||
|
Role = request.Role,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.RealmMembers.Add(member);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.members.invite",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||||
|
{ "role", Value.ForNumber(request.Role) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
member.Account = relatedUser;
|
||||||
|
member.Realm = realm;
|
||||||
|
await rs.SendInviteNotify(member);
|
||||||
|
|
||||||
|
return Ok(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("invites/{slug}/accept")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<Realm>> AcceptMemberInvite(string slug)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == accountId)
|
||||||
|
.Where(m => m.Realm.Slug == slug)
|
||||||
|
.Where(m => m.JoinedAt == null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
|
member.JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||||
|
db.Update(member);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.members.join",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(member.AccountId.ToString()) }
|
||||||
|
},
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("invites/{slug}/decline")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == accountId)
|
||||||
|
.Where(m => m.Realm.Slug == slug)
|
||||||
|
.Where(m => m.JoinedAt == null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
als.CreateActionLogFromRequest(
|
||||||
|
ActionLogType.RealmLeave,
|
||||||
|
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||||
|
Request
|
||||||
|
);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("{slug}/members")]
|
||||||
|
public async Task<ActionResult<List<RealmMember>>> ListMembers(
|
||||||
|
string slug,
|
||||||
|
[FromQuery] int offset = 0,
|
||||||
|
[FromQuery] int take = 20,
|
||||||
|
[FromQuery] bool withStatus = false,
|
||||||
|
[FromQuery] string? status = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(r => r.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
if (!realm.IsPublic)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal))
|
||||||
|
return StatusCode(403, "You must be a member to view this realm's members.");
|
||||||
|
}
|
||||||
|
|
||||||
|
IQueryable<RealmMember> query = db.RealmMembers
|
||||||
|
.Where(m => m.RealmId == realm.Id)
|
||||||
|
.Where(m => m.LeaveAt == null)
|
||||||
|
.Include(m => m.Account)
|
||||||
|
.Include(m => m.Account.Profile);
|
||||||
|
|
||||||
|
if (withStatus)
|
||||||
|
{
|
||||||
|
var members = await query
|
||||||
|
.OrderBy(m => m.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList());
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(status))
|
||||||
|
{
|
||||||
|
members = members.Where(m =>
|
||||||
|
memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null &&
|
||||||
|
s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var total = members.Count;
|
||||||
|
Response.Headers["X-Total"] = total.ToString();
|
||||||
|
|
||||||
|
var result = members.Skip(offset).Take(take).ToList();
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
Response.Headers["X-Total"] = total.ToString();
|
||||||
|
|
||||||
|
var members = await query
|
||||||
|
.OrderBy(m => m.CreatedAt)
|
||||||
|
.Skip(offset)
|
||||||
|
.Take(take)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(members);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("{slug}/members/me")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<RealmMember>> GetCurrentIdentity(string slug)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == accountId)
|
||||||
|
.Where(m => m.Realm.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (member is null) return NotFound();
|
||||||
|
return Ok(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{slug}/members/me")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> LeaveRealm(string slug)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == accountId)
|
||||||
|
.Where(m => m.Realm.Slug == slug)
|
||||||
|
.Where(m => m.JoinedAt != null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
|
if (member.Role == RealmMemberRole.Owner)
|
||||||
|
return StatusCode(403, "Owner cannot leave their own realm.");
|
||||||
|
|
||||||
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
als.CreateActionLogFromRequest(
|
||||||
|
ActionLogType.RealmLeave,
|
||||||
|
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||||
|
Request
|
||||||
|
);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RealmRequest
|
||||||
|
{
|
||||||
|
[MaxLength(1024)] public string? Slug { get; set; }
|
||||||
|
[MaxLength(1024)] public string? Name { get; set; }
|
||||||
|
[MaxLength(4096)] public string? Description { get; set; }
|
||||||
|
public string? PictureId { get; set; }
|
||||||
|
public string? BackgroundId { get; set; }
|
||||||
|
public bool? IsCommunity { get; set; }
|
||||||
|
public bool? IsPublic { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<Realm>> CreateRealm(RealmRequest request)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name.");
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug.");
|
||||||
|
|
||||||
|
var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug);
|
||||||
|
if (slugExists) return BadRequest("Realm with this slug already exists.");
|
||||||
|
|
||||||
|
var realm = new Realm
|
||||||
|
{
|
||||||
|
Name = request.Name!,
|
||||||
|
Slug = request.Slug!,
|
||||||
|
Description = request.Description!,
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
IsCommunity = request.IsCommunity ?? false,
|
||||||
|
IsPublic = request.IsPublic ?? false,
|
||||||
|
Members = new List<RealmMember>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Role = RealmMemberRole.Owner,
|
||||||
|
AccountId = Guid.Parse(currentUser.Id),
|
||||||
|
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.PictureId is not null)
|
||||||
|
{
|
||||||
|
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||||
|
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.BackgroundId is not null)
|
||||||
|
{
|
||||||
|
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||||
|
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||||
|
realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Realms.Add(realm);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.create",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "name", Value.ForString(realm.Name) },
|
||||||
|
{ "slug", Value.ForString(realm.Slug) },
|
||||||
|
{ "is_community", Value.ForBool(realm.IsCommunity) },
|
||||||
|
{ "is_public", Value.ForBool(realm.IsPublic) }
|
||||||
|
},
|
||||||
|
UserId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
var realmResourceId = $"realm:{realm.Id}";
|
||||||
|
|
||||||
|
if (realm.Picture is not null)
|
||||||
|
{
|
||||||
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
|
{
|
||||||
|
FileId = realm.Picture.Id,
|
||||||
|
Usage = "realm.picture",
|
||||||
|
ResourceId = realmResourceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realm.Background is not null)
|
||||||
|
{
|
||||||
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
|
{
|
||||||
|
FileId = realm.Background.Id,
|
||||||
|
Usage = "realm.background",
|
||||||
|
ResourceId = realmResourceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{slug}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<Realm>> Update(string slug, [FromBody] RealmRequest request)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(r => r.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member is null || member.Role < RealmMemberRole.Moderator)
|
||||||
|
return StatusCode(403, "You do not have permission to update this realm.");
|
||||||
|
|
||||||
|
if (request.Slug is not null && request.Slug != realm.Slug)
|
||||||
|
{
|
||||||
|
var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug);
|
||||||
|
if (slugExists) return BadRequest("Realm with this slug already exists.");
|
||||||
|
realm.Slug = request.Slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Name is not null)
|
||||||
|
realm.Name = request.Name;
|
||||||
|
if (request.Description is not null)
|
||||||
|
realm.Description = request.Description;
|
||||||
|
if (request.IsCommunity is not null)
|
||||||
|
realm.IsCommunity = request.IsCommunity.Value;
|
||||||
|
if (request.IsPublic is not null)
|
||||||
|
realm.IsPublic = request.IsPublic.Value;
|
||||||
|
|
||||||
|
if (request.PictureId is not null)
|
||||||
|
{
|
||||||
|
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||||
|
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
|
||||||
|
// Remove old references for the realm picture
|
||||||
|
if (realm.Picture is not null)
|
||||||
|
{
|
||||||
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||||
|
{
|
||||||
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult);
|
||||||
|
|
||||||
|
// Create a new reference
|
||||||
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
|
{
|
||||||
|
FileId = realm.Picture.Id,
|
||||||
|
Usage = "realm.picture",
|
||||||
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.BackgroundId is not null)
|
||||||
|
{
|
||||||
|
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||||
|
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||||
|
|
||||||
|
// Remove old references for the realm background
|
||||||
|
if (realm.Background is not null)
|
||||||
|
{
|
||||||
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||||
|
{
|
||||||
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult);
|
||||||
|
|
||||||
|
// Create a new reference
|
||||||
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
|
{
|
||||||
|
FileId = realm.Background.Id,
|
||||||
|
Usage = "realm.background",
|
||||||
|
ResourceId = realm.ResourceIdentifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Realms.Update(realm);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.update",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "name_updated", Value.ForBool(request.Name != null) },
|
||||||
|
{ "slug_updated", Value.ForBool(request.Slug != null) },
|
||||||
|
{ "description_updated", Value.ForBool(request.Description != null) },
|
||||||
|
{ "picture_updated", Value.ForBool(request.PictureId != null) },
|
||||||
|
{ "background_updated", Value.ForBool(request.BackgroundId != null) },
|
||||||
|
{ "is_community_updated", Value.ForBool(request.IsCommunity != null) },
|
||||||
|
{ "is_public_updated", Value.ForBool(request.IsPublic != null) }
|
||||||
|
},
|
||||||
|
UserId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{slug}/members/me")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<RealmMember>> JoinRealm(string slug)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(r => r.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
if (!realm.IsCommunity)
|
||||||
|
return StatusCode(403, "Only community realms can be joined without invitation.");
|
||||||
|
|
||||||
|
var existingMember = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (existingMember is not null)
|
||||||
|
return BadRequest("You are already a member of this realm.");
|
||||||
|
|
||||||
|
var member = new RealmMember
|
||||||
|
{
|
||||||
|
AccountId = currentUser.Id,
|
||||||
|
RealmId = realm.Id,
|
||||||
|
Role = RealmMemberRole.Normal,
|
||||||
|
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||||
|
};
|
||||||
|
|
||||||
|
db.RealmMembers.Add(member);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.members.join",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(currentUser.Id) },
|
||||||
|
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
||||||
|
},
|
||||||
|
UserId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{slug}/members/{memberId:guid}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> RemoveMember(string slug, Guid memberId)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(r => r.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role))
|
||||||
|
return StatusCode(403, "You do not have permission to remove members from this realm.");
|
||||||
|
|
||||||
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.members.kick",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||||
|
{ "kicker_id", Value.ForString(currentUser.Id) }
|
||||||
|
},
|
||||||
|
UserId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{slug}/members/{memberId:guid}/role")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<RealmMember>> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole)
|
||||||
|
{
|
||||||
|
if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role.");
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(r => r.Slug == slug)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
|
||||||
|
.Include(m => m.Account)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role,
|
||||||
|
newRole))
|
||||||
|
return StatusCode(403, "You do not have permission to update member roles in this realm.");
|
||||||
|
|
||||||
|
member.Role = newRole;
|
||||||
|
db.RealmMembers.Update(member);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.members.role_update",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||||
|
{ "new_role", Value.ForNumber(newRole) },
|
||||||
|
{ "updater_id", Value.ForString(currentUser.Id) }
|
||||||
|
},
|
||||||
|
UserId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{slug}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> Delete(string slug)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var realm = await db.Realms
|
||||||
|
.Where(r => r.Slug == slug)
|
||||||
|
.Include(r => r.Picture)
|
||||||
|
.Include(r => r.Background)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
|
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner))
|
||||||
|
return StatusCode(403, "Only the owner can delete this realm.");
|
||||||
|
|
||||||
|
db.Realms.Remove(realm);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||||
|
{
|
||||||
|
Action = "realms.delete",
|
||||||
|
Meta =
|
||||||
|
{
|
||||||
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
|
{ "realm_name", Value.ForString(realm.Name) },
|
||||||
|
{ "realm_slug", Value.ForString(realm.Slug) }
|
||||||
|
},
|
||||||
|
UserId = currentUser.Id,
|
||||||
|
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||||
|
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete all file references for this realm
|
||||||
|
var realmResourceId = $"realm:{realm.Id}";
|
||||||
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||||
|
{
|
||||||
|
ResourceId = realmResourceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,22 +1,36 @@
|
|||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Shared;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Realm;
|
namespace DysonNetwork.Sphere.Realm;
|
||||||
|
|
||||||
public class RealmService(AppDatabase db, NotificationService nty, IStringLocalizer<NotificationResource> localizer)
|
public class RealmService(
|
||||||
|
AppDatabase db,
|
||||||
|
PusherService.PusherServiceClient pusher,
|
||||||
|
AccountService.AccountServiceClient accounts,
|
||||||
|
IStringLocalizer<NotificationResource> localizer
|
||||||
|
)
|
||||||
{
|
{
|
||||||
public async Task SendInviteNotify(RealmMember member)
|
public async Task SendInviteNotify(RealmMember member)
|
||||||
{
|
{
|
||||||
AccountService.SetCultureInfo(member.Account);
|
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
|
||||||
await nty.SendNotification(
|
CultureService.SetCultureInfo(account);
|
||||||
member.Account,
|
|
||||||
"invites.realms",
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
localizer["RealmInviteTitle"],
|
new SendPushNotificationToUserRequest
|
||||||
null,
|
{
|
||||||
localizer["RealmInviteBody", member.Realm.Name],
|
UserId = account.Id,
|
||||||
actionUri: "/realms"
|
Notification = new PushNotification
|
||||||
|
{
|
||||||
|
Topic = "invites.realms",
|
||||||
|
Title = localizer["RealmInviteTitle"],
|
||||||
|
Body = localizer["RealmInviteBody", member.Realm.Name],
|
||||||
|
ActionUri = "/realms",
|
||||||
|
IsSavable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,21 +1,17 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using DysonNetwork.Sphere.Connection;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
using tusdotnet;
|
using tusdotnet;
|
||||||
using tusdotnet.Stores;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Startup;
|
namespace DysonNetwork.Sphere.Startup;
|
||||||
|
|
||||||
public static class ApplicationConfiguration
|
public static class ApplicationConfiguration
|
||||||
{
|
{
|
||||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration, TusDiskStore tusDiskStore)
|
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
app.MapMetrics();
|
app.MapMetrics();
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
app.UseMiddleware<ClientTypeMiddleware>();
|
|
||||||
|
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
@@ -44,8 +40,6 @@ public static class ApplicationConfiguration
|
|||||||
app.MapStaticAssets().RequireRateLimiting("fixed");
|
app.MapStaticAssets().RequireRateLimiting("fixed");
|
||||||
app.MapRazorPages().RequireRateLimiting("fixed");
|
app.MapRazorPages().RequireRateLimiting("fixed");
|
||||||
|
|
||||||
app.MapTus("/files/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusDiskStore)));
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using DysonNetwork.Sphere.Storage.Handlers;
|
|
||||||
using DysonNetwork.Sphere.Wallet;
|
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Startup;
|
namespace DysonNetwork.Sphere.Startup;
|
||||||
@@ -19,63 +16,15 @@ public static class ScheduledJobsConfiguration
|
|||||||
.WithIdentity("AppDatabaseRecyclingTrigger")
|
.WithIdentity("AppDatabaseRecyclingTrigger")
|
||||||
.WithCronSchedule("0 0 0 * * ?"));
|
.WithCronSchedule("0 0 0 * * ?"));
|
||||||
|
|
||||||
var cloudFilesRecyclingJob = new JobKey("CloudFilesUnusedRecycling");
|
// var postViewFlushJob = new JobKey("PostViewFlush");
|
||||||
q.AddJob<CloudFileUnusedRecyclingJob>(opts => opts.WithIdentity(cloudFilesRecyclingJob));
|
// q.AddJob<PostViewFlushJob>(opts => opts.WithIdentity(postViewFlushJob));
|
||||||
q.AddTrigger(opts => opts
|
// q.AddTrigger(opts => opts
|
||||||
.ForJob(cloudFilesRecyclingJob)
|
// .ForJob(postViewFlushJob)
|
||||||
.WithIdentity("CloudFilesUnusedRecyclingTrigger")
|
// .WithIdentity("PostViewFlushTrigger")
|
||||||
.WithSimpleSchedule(o => o.WithIntervalInHours(1).RepeatForever())
|
// .WithSimpleSchedule(o => o
|
||||||
);
|
// .WithIntervalInMinutes(1)
|
||||||
|
// .RepeatForever())
|
||||||
var actionLogFlushJob = new JobKey("ActionLogFlush");
|
// );
|
||||||
q.AddJob<ActionLogFlushJob>(opts => opts.WithIdentity(actionLogFlushJob));
|
|
||||||
q.AddTrigger(opts => opts
|
|
||||||
.ForJob(actionLogFlushJob)
|
|
||||||
.WithIdentity("ActionLogFlushTrigger")
|
|
||||||
.WithSimpleSchedule(o => o
|
|
||||||
.WithIntervalInMinutes(5)
|
|
||||||
.RepeatForever())
|
|
||||||
);
|
|
||||||
|
|
||||||
var readReceiptFlushJob = new JobKey("ReadReceiptFlush");
|
|
||||||
q.AddJob<ReadReceiptFlushJob>(opts => opts.WithIdentity(readReceiptFlushJob));
|
|
||||||
q.AddTrigger(opts => opts
|
|
||||||
.ForJob(readReceiptFlushJob)
|
|
||||||
.WithIdentity("ReadReceiptFlushTrigger")
|
|
||||||
.WithSimpleSchedule(o => o
|
|
||||||
.WithIntervalInSeconds(60)
|
|
||||||
.RepeatForever())
|
|
||||||
);
|
|
||||||
|
|
||||||
var lastActiveFlushJob = new JobKey("LastActiveFlush");
|
|
||||||
q.AddJob<LastActiveFlushJob>(opts => opts.WithIdentity(lastActiveFlushJob));
|
|
||||||
q.AddTrigger(opts => opts
|
|
||||||
.ForJob(lastActiveFlushJob)
|
|
||||||
.WithIdentity("LastActiveFlushTrigger")
|
|
||||||
.WithSimpleSchedule(o => o
|
|
||||||
.WithIntervalInMinutes(5)
|
|
||||||
.RepeatForever())
|
|
||||||
);
|
|
||||||
|
|
||||||
var postViewFlushJob = new JobKey("PostViewFlush");
|
|
||||||
q.AddJob<PostViewFlushJob>(opts => opts.WithIdentity(postViewFlushJob));
|
|
||||||
q.AddTrigger(opts => opts
|
|
||||||
.ForJob(postViewFlushJob)
|
|
||||||
.WithIdentity("PostViewFlushTrigger")
|
|
||||||
.WithSimpleSchedule(o => o
|
|
||||||
.WithIntervalInMinutes(1)
|
|
||||||
.RepeatForever())
|
|
||||||
);
|
|
||||||
|
|
||||||
var subscriptionRenewalJob = new JobKey("SubscriptionRenewal");
|
|
||||||
q.AddJob<SubscriptionRenewalJob>(opts => opts.WithIdentity(subscriptionRenewalJob));
|
|
||||||
q.AddTrigger(opts => opts
|
|
||||||
.ForJob(subscriptionRenewalJob)
|
|
||||||
.WithIdentity("SubscriptionRenewalTrigger")
|
|
||||||
.WithSimpleSchedule(o => o
|
|
||||||
.WithIntervalInMinutes(30)
|
|
||||||
.RepeatForever())
|
|
||||||
);
|
|
||||||
|
|
||||||
var webFeedScraperJob = new JobKey("WebFeedScraper");
|
var webFeedScraperJob = new JobKey("WebFeedScraper");
|
||||||
q.AddJob<WebFeedScraperJob>(opts => opts.WithIdentity(webFeedScraperJob));
|
q.AddJob<WebFeedScraperJob>(opts => opts.WithIdentity(webFeedScraperJob));
|
||||||
|
@@ -21,9 +21,7 @@ using DysonNetwork.Shared.Proto;
|
|||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using DysonNetwork.Sphere.Developer;
|
using DysonNetwork.Sphere.Developer;
|
||||||
using DysonNetwork.Sphere.Discovery;
|
using DysonNetwork.Sphere.Discovery;
|
||||||
using DysonNetwork.Sphere.Safety;
|
|
||||||
using tusdotnet.Stores;
|
using tusdotnet.Stores;
|
||||||
using PermissionService = DysonNetwork.Sphere.Permission.PermissionService;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Startup;
|
namespace DysonNetwork.Sphere.Startup;
|
||||||
|
|
||||||
@@ -90,12 +88,6 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddCors();
|
services.AddCors();
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
services.AddAuthentication(options =>
|
|
||||||
{
|
|
||||||
options.DefaultAuthenticateScheme = AuthConstants.SchemeName;
|
|
||||||
options.DefaultChallengeScheme = AuthConstants.SchemeName;
|
|
||||||
})
|
|
||||||
.AddScheme<DysonTokenAuthOptions, DysonTokenAuthHandler>(AuthConstants.SchemeName, _ => { });
|
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -146,17 +138,6 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
var tusStorePath = configuration.GetSection("Tus").GetValue<string>("StorePath")!;
|
|
||||||
Directory.CreateDirectory(tusStorePath);
|
|
||||||
var tusDiskStore = new TusDiskStore(tusStorePath);
|
|
||||||
|
|
||||||
services.AddSingleton(tusDiskStore);
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services)
|
public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<FlushBufferService>();
|
services.AddSingleton<FlushBufferService>();
|
||||||
@@ -169,7 +150,6 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.Configure<GeoIpOptions>(configuration.GetSection("GeoIP"));
|
services.Configure<GeoIpOptions>(configuration.GetSection("GeoIP"));
|
||||||
services.AddScoped<GeoIpService>();
|
services.AddScoped<GeoIpService>();
|
||||||
services.AddScoped<PermissionService>();
|
|
||||||
services.AddScoped<PublisherService>();
|
services.AddScoped<PublisherService>();
|
||||||
services.AddScoped<PublisherSubscriptionService>();
|
services.AddScoped<PublisherSubscriptionService>();
|
||||||
services.AddScoped<ActivityService>();
|
services.AddScoped<ActivityService>();
|
||||||
@@ -181,7 +161,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IRealtimeService, LiveKitRealtimeService>();
|
services.AddScoped<IRealtimeService, LiveKitRealtimeService>();
|
||||||
services.AddScoped<WebReaderService>();
|
services.AddScoped<WebReaderService>();
|
||||||
services.AddScoped<WebFeedService>();
|
services.AddScoped<WebFeedService>();
|
||||||
services.AddScoped<SafetyService>();
|
|
||||||
services.AddScoped<DiscoveryService>();
|
services.AddScoped<DiscoveryService>();
|
||||||
services.AddScoped<CustomAppService>();
|
services.AddScoped<CustomAppService>();
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@@ -23,12 +23,12 @@ public class StickerService(
|
|||||||
db.Stickers.Add(sticker);
|
db.Stickers.Add(sticker);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
var stickerResourceId = $"sticker:{sticker.Id}";
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
await fileRefService.CreateReferenceAsync(
|
{
|
||||||
sticker.Image.Id,
|
FileId = sticker.Image.Id,
|
||||||
StickerFileUsageIdentifier,
|
Usage = StickerFileUsageIdentifier,
|
||||||
stickerResourceId
|
ResourceId = sticker.ResourceIdentifier
|
||||||
);
|
});
|
||||||
|
|
||||||
return sticker;
|
return sticker;
|
||||||
}
|
}
|
||||||
@@ -37,24 +37,17 @@ public class StickerService(
|
|||||||
{
|
{
|
||||||
if (newImage is not null)
|
if (newImage is not null)
|
||||||
{
|
{
|
||||||
var stickerResourceId = $"sticker:{sticker.Id}";
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = sticker.ResourceIdentifier });
|
||||||
|
|
||||||
// Delete old references
|
sticker.Image = newImage;
|
||||||
var oldRefs =
|
|
||||||
await fileRefService.GetResourceReferencesAsync(stickerResourceId, StickerFileUsageIdentifier);
|
|
||||||
foreach (var oldRef in oldRefs)
|
|
||||||
{
|
|
||||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
sticker.Image = newImage.ToReferenceObject();
|
|
||||||
|
|
||||||
// Create new reference
|
// Create new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||||
newImage.Id,
|
{
|
||||||
StickerFileUsageIdentifier,
|
FileId = newImage.Id,
|
||||||
stickerResourceId
|
Usage = StickerFileUsageIdentifier,
|
||||||
);
|
ResourceId = sticker.ResourceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Stickers.Update(sticker);
|
db.Stickers.Update(sticker);
|
||||||
@@ -71,7 +64,7 @@ public class StickerService(
|
|||||||
var stickerResourceId = $"sticker:{sticker.Id}";
|
var stickerResourceId = $"sticker:{sticker.Id}";
|
||||||
|
|
||||||
// Delete all file references for this sticker
|
// Delete all file references for this sticker
|
||||||
await fileRefService.DeleteResourceReferencesAsync(stickerResourceId);
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = stickerResourceId });
|
||||||
|
|
||||||
db.Stickers.Remove(sticker);
|
db.Stickers.Remove(sticker);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@@ -90,13 +83,11 @@ public class StickerService(
|
|||||||
|
|
||||||
// Delete all file references for each sticker in the pack
|
// Delete all file references for each sticker in the pack
|
||||||
foreach (var stickerResourceId in stickers.Select(sticker => $"sticker:{sticker.Id}"))
|
foreach (var stickerResourceId in stickers.Select(sticker => $"sticker:{sticker.Id}"))
|
||||||
{
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = stickerResourceId });
|
||||||
await fileRefService.DeleteResourceReferencesAsync(stickerResourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete any references for the pack itself
|
// Delete any references for the pack itself
|
||||||
var packResourceId = $"stickerpack:{pack.Id}";
|
var packResourceId = $"stickerpack:{pack.Id}";
|
||||||
await fileRefService.DeleteResourceReferencesAsync(packResourceId);
|
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = packResourceId });
|
||||||
|
|
||||||
db.Stickers.RemoveRange(stickers);
|
db.Stickers.RemoveRange(stickers);
|
||||||
db.StickerPacks.Remove(pack);
|
db.StickerPacks.Remove(pack);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Shared.Auth;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
@@ -13,84 +13,6 @@
|
|||||||
"FastRetrieve": "localhost:6379",
|
"FastRetrieve": "localhost:6379",
|
||||||
"Etcd": "etcd.orb.local:2379"
|
"Etcd": "etcd.orb.local:2379"
|
||||||
},
|
},
|
||||||
"Authentication": {
|
|
||||||
"Schemes": {
|
|
||||||
"Bearer": {
|
|
||||||
"ValidAudiences": [
|
|
||||||
"http://localhost:5071",
|
|
||||||
"https://localhost:7099"
|
|
||||||
],
|
|
||||||
"ValidIssuer": "solar-network"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AuthToken": {
|
|
||||||
"PublicKeyPath": "Keys/PublicKey.pem",
|
|
||||||
"PrivateKeyPath": "Keys/PrivateKey.pem"
|
|
||||||
},
|
|
||||||
"OidcProvider": {
|
|
||||||
"IssuerUri": "https://nt.solian.app",
|
|
||||||
"PublicKeyPath": "Keys/PublicKey.pem",
|
|
||||||
"PrivateKeyPath": "Keys/PrivateKey.pem",
|
|
||||||
"AccessTokenLifetime": "01:00:00",
|
|
||||||
"RefreshTokenLifetime": "30.00:00:00",
|
|
||||||
"AuthorizationCodeLifetime": "00:30:00",
|
|
||||||
"RequireHttpsMetadata": true
|
|
||||||
},
|
|
||||||
"Tus": {
|
|
||||||
"StorePath": "Uploads"
|
|
||||||
},
|
|
||||||
"Storage": {
|
|
||||||
"PreferredRemote": "minio",
|
|
||||||
"Remote": [
|
|
||||||
{
|
|
||||||
"Id": "minio",
|
|
||||||
"Label": "Minio",
|
|
||||||
"Region": "auto",
|
|
||||||
"Bucket": "solar-network-development",
|
|
||||||
"Endpoint": "localhost:9000",
|
|
||||||
"SecretId": "littlesheep",
|
|
||||||
"SecretKey": "password",
|
|
||||||
"EnabledSigned": true,
|
|
||||||
"EnableSsl": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id": "cloudflare",
|
|
||||||
"Label": "Cloudflare R2",
|
|
||||||
"Region": "auto",
|
|
||||||
"Bucket": "solar-network",
|
|
||||||
"Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com",
|
|
||||||
"SecretId": "8ff5d06c7b1639829d60bc6838a542e6",
|
|
||||||
"SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67",
|
|
||||||
"EnableSigned": true,
|
|
||||||
"EnableSsl": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Captcha": {
|
|
||||||
"Provider": "cloudflare",
|
|
||||||
"ApiKey": "0x4AAAAAABCDUdOujj4feOb_",
|
|
||||||
"ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U"
|
|
||||||
},
|
|
||||||
"Notifications": {
|
|
||||||
"Topic": "dev.solsynth.solian",
|
|
||||||
"Endpoint": "http://localhost:8088"
|
|
||||||
},
|
|
||||||
"Email": {
|
|
||||||
"Server": "smtp4dev.orb.local",
|
|
||||||
"Port": 25,
|
|
||||||
"UseSsl": false,
|
|
||||||
"Username": "no-reply@mail.solsynth.dev",
|
|
||||||
"Password": "password",
|
|
||||||
"FromAddress": "no-reply@mail.solsynth.dev",
|
|
||||||
"FromName": "Alphabot",
|
|
||||||
"SubjectPrefix": "Solar Network"
|
|
||||||
},
|
|
||||||
"RealtimeChat": {
|
|
||||||
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
|
||||||
"ApiKey": "APIs6TiL8wj3A4j",
|
|
||||||
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
|
|
||||||
},
|
|
||||||
"GeoIp": {
|
"GeoIp": {
|
||||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
||||||
},
|
},
|
||||||
@@ -111,23 +33,17 @@
|
|||||||
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
|
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Payment": {
|
|
||||||
"Auth": {
|
|
||||||
"Afdian": "<token here>"
|
|
||||||
},
|
|
||||||
"Subscriptions": {
|
|
||||||
"Afdian": {
|
|
||||||
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
|
|
||||||
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
|
|
||||||
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"KnownProxies": [
|
"KnownProxies": [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
"::1"
|
"::1"
|
||||||
],
|
],
|
||||||
"Etcd": {
|
"Etcd": {
|
||||||
"Insecure": true
|
"Insecure": true
|
||||||
|
},
|
||||||
|
"Service": {
|
||||||
|
"Name": "DysonNetwork.Sphere",
|
||||||
|
"Url": "https://localhost:7099",
|
||||||
|
"ClientCert": "../Certificates/client.crt",
|
||||||
|
"ClientKey": "../Certificates/client.key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user