♻️ Extract the Developer to new service, add PublisherServiceGrpc

This commit is contained in:
2025-08-07 17:16:38 +08:00
parent f1ea7c1c5a
commit 00cdd1bc5d
35 changed files with 602 additions and 101 deletions

View File

@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Develop;
public class AppDatabase(
DbContextOptions<AppDatabase> options,
IConfiguration configuration
) : DbContext(options)
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(
configuration.GetConnectionString("App"),
opt => opt
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
.UseNodaTime()
).UseSnakeCaseNamingConvention();
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}

View File

@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"]
RUN dotnet restore "DysonNetwork.Develop/DysonNetwork.Develop.csproj"
COPY . .
WORKDIR "/src/DysonNetwork.Develop"
RUN dotnet build "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DysonNetwork.Develop.dll"]

View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
<PackageReference Include="NodaTime" Version="3.2.2"/>
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using NodaTime;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Develop.Identity;
public enum CustomAppStatus
{

View File

@@ -1,10 +1,9 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Develop.Identity;
[ApiController]
[Route("/api/developers/{pubName}/apps")]

View File

@@ -1,8 +1,7 @@
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Develop.Identity;
public class CustomAppService(
AppDatabase db,

View File

@@ -1,18 +1,16 @@
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Develop.Identity;
[ApiController]
[Route("/api/developers")]
public class DeveloperController(
AppDatabase db,
PublisherService ps,
PublisherService.PublisherServiceClient ps,
ActionLogService.ActionLogServiceClient als
)
: ControllerBase

View File

@@ -0,0 +1,29 @@
using DysonNetwork.Develop;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Develop.Startup;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.ConfigureAppKestrel(builder.Configuration);
builder.Services.AddRegistryService(builder.Configuration);
builder.Services.AddAppServices(builder.Configuration);
builder.Services.AddAppAuthentication();
builder.Services.AddAppSwagger();
builder.Services.AddDysonAuth();
builder.Services.AddPublisherService();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
await db.Database.MigrateAsync();
}
app.ConfigureAppMiddleware(builder.Configuration);
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5156",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7192;http://localhost:5156",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Net;
using DysonNetwork.Shared.Auth;
using Microsoft.AspNetCore.HttpOverrides;
using Prometheus;
namespace DysonNetwork.Develop.Startup;
public static class ApplicationConfiguration
{
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
{
app.MapMetrics();
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
app.UseRequestLocalization();
ConfigureForwardedHeaders(app, configuration);
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<PermissionMiddleware>();
app.MapControllers();
return app;
}
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
{
var knownProxiesSection = configuration.GetSection("KnownProxies");
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
if (knownProxiesSection.Exists())
{
var proxyAddresses = knownProxiesSection.Get<string[]>();
if (proxyAddresses != null)
foreach (var proxy in proxyAddresses)
if (IPAddress.TryParse(proxy, out var ipAddress))
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
}
else
{
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
}
app.UseForwardedHeaders(forwardedHeadersOptions);
}
}

View File

@@ -0,0 +1,68 @@
using System.Globalization;
using Microsoft.OpenApi.Models;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
using System.Text.Json;
using DysonNetwork.Shared.Cache;
namespace DysonNetwork.Develop.Startup;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddLocalization();
services.AddDbContext<AppDatabase>();
services.AddSingleton<IClock>(SystemClock.Instance);
services.AddHttpContextAccessor();
services.AddSingleton<ICacheService, CacheServiceRedis>();
services.AddHttpClient();
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
});
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("zh-Hans"),
};
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
return services;
}
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
{
services.AddCors();
services.AddAuthorization();
return services;
}
public static IServiceCollection AddAppSwagger(this IServiceCollection services)
{
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Develop API",
});
});
services.AddOpenApi();
return services;
}
}

View File

@@ -0,0 +1,30 @@
{
"Debug": true,
"BaseUrl": "http://localhost:5071",
"SiteUrl": "https://solian.app",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379",
"Etcd": "etcd.orb.local:2379"
},
"KnownProxies": [
"127.0.0.1",
"::1"
],
"Etcd": {
"Insecure": true
},
"Service": {
"Name": "DysonNetwork.Develop",
"Url": "https://localhost:7099",
"ClientCert": "../Certificates/client.crt",
"ClientKey": "../Certificates/client.key"
}
}

View File

@@ -123,4 +123,16 @@ public static class GrpcClientHelper
return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<PublisherService.PublisherServiceClient> CreatePublisherServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Sphere");
return new PublisherService.PublisherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
}

View File

@@ -0,0 +1,108 @@
syntax = "proto3";
package proto;
option csharp_namespace = "DysonNetwork.Shared.Proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "file.proto";
enum PublisherType {
PUBLISHER_TYPE_UNSPECIFIED = 0;
INDIVIDUAL = 1;
ORGANIZATIONAL = 2;
}
enum PublisherMemberRole {
PUBLISHER_MEMBER_ROLE_UNSPECIFIED = 0;
OWNER = 100;
MANAGER = 75;
EDITOR = 50;
VIEWER = 25;
}
message PublisherFeature {
string id = 1;
string flag = 2;
google.protobuf.Timestamp expired_at = 3;
string publisher_id = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
message PublisherMember {
string publisher_id = 1;
string account_id = 2;
PublisherMemberRole role = 3;
google.protobuf.Timestamp joined_at = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
message Publisher {
string id = 1;
PublisherType type = 2;
string name = 3;
string nick = 4;
google.protobuf.StringValue bio = 5;
CloudFile picture = 8;
CloudFile background = 9;
optional bytes verification_mark = 10;
string account_id = 11;
string realm_id = 12;
google.protobuf.Timestamp created_at = 13;
google.protobuf.Timestamp updated_at = 14;
}
message GetPublisherRequest {
string name = 1;
}
message GetPublisherResponse {
Publisher publisher = 1;
}
message ListPublishersRequest {
string account_id = 1; // filter by owner/member account
string realm_id = 2; // filter by realm
int32 page_size = 3;
string page_token = 4;
string order_by = 5;
}
message ListPublishersResponse {
repeated Publisher publishers = 1;
string next_page_token = 2;
int32 total_size = 3;
}
message ListPublisherMembersRequest {
string publisher_id = 1;
}
message ListPublisherMembersResponse {
repeated PublisherMember members = 1;
}
message SetPublisherFeatureFlagRequest {
string publisher_id = 1;
string flag = 2;
}
message HasPublisherFeatureRequest {
string publisher_id = 1;
string flag = 2;
}
message HasPublisherFeatureResponse {
bool enabled = 1;
}
service PublisherService {
rpc GetPublisher(GetPublisherRequest) returns (GetPublisherResponse);
rpc ListPublishers(ListPublishersRequest) returns (ListPublishersResponse);
rpc ListPublisherMembers(ListPublisherMembersRequest) returns (ListPublisherMembersResponse);
rpc SetPublisherFeatureFlag(SetPublisherFeatureFlagRequest) returns (google.protobuf.StringValue); // returns optional message
rpc HasPublisherFeature(HasPublisherFeatureRequest) returns (HasPublisherFeatureResponse);
}

View File

@@ -92,4 +92,23 @@ public static class ServiceInjectionHelper
return services;
}
public static IServiceCollection AddPublisherService(this IServiceCollection services)
{
services.AddSingleton<PublisherService.PublisherServiceClient>(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreatePublisherServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
});
return services;
}
}

View File

@@ -10,7 +10,7 @@ namespace DysonNetwork.Sphere.Activity;
public class ActivityService(
AppDatabase db,
PublisherService pub,
Publisher.PublisherService pub,
PostService ps,
DiscoveryService ds,
AccountService.AccountServiceClient accounts

View File

@@ -1,7 +1,6 @@
using System.Linq.Expressions;
using System.Reflection;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Publisher;
using DysonNetwork.Sphere.Realm;
@@ -61,9 +60,6 @@ public class AppDatabase(
public DbSet<StickerPack> StickerPacks { get; set; }
public DbSet<StickerPackOwnership> StickerPackOwnerships { get; set; }
public DbSet<CustomApp> CustomApps { get; set; }
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; }
public DbSet<WebReader.WebArticle> WebArticles { get; set; }
public DbSet<WebReader.WebFeed> WebFeeds { get; set; }
@@ -103,16 +99,6 @@ public class AppDatabase(
.HasIndex(p => p.SearchVector)
.HasMethod("GIN");
modelBuilder.Entity<CustomAppSecret>()
.HasIndex(s => s.Secret)
.IsUnique();
modelBuilder.Entity<CustomApp>()
.HasMany(c => c.Secrets)
.WithOne(s => s.App)
.HasForeignKey(s => s.AppId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Post.Post>()
.HasOne(p => p.RepliedPost)
.WithMany()

View File

@@ -36,6 +36,7 @@
</PackageReference>
<PackageReference Include="NodaTime" Version="3.2.2"/>
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -397,20 +396,12 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<CustomAppLinks>("Links")
.HasColumnType("jsonb")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<CustomAppOauthConfig>("OauthConfig")
.HasColumnType("jsonb")
.HasColumnName("oauth_config");
b.Property<CloudFileReferenceObject>("Picture")
.HasColumnType("jsonb")
.HasColumnName("picture");

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
@@ -248,8 +247,6 @@ namespace DysonNetwork.Sphere.Migrations
picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true),
links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true),
publisher_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -397,20 +396,12 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<CustomAppLinks>("Links")
.HasColumnType("jsonb")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<CustomAppOauthConfig>("OauthConfig")
.HasColumnType("jsonb")
.HasColumnName("oauth_config");
b.Property<CloudFileReferenceObject>("Picture")
.HasColumnType("jsonb")
.HasColumnName("picture");

View File

@@ -5,7 +5,6 @@ using System.Text.Json;
using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Poll;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore;
@@ -399,20 +398,12 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<CustomAppLinks>("Links")
.HasColumnType("jsonb")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<CustomAppOauthConfig>("OauthConfig")
.HasColumnType("jsonb")
.HasColumnName("oauth_config");
b.Property<CloudFileReferenceObject>("Picture")
.HasColumnType("jsonb")
.HasColumnName("picture");

View File

@@ -5,7 +5,6 @@ using System.Text.Json;
using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Poll;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore;
@@ -399,20 +398,12 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<CustomAppLinks>("Links")
.HasColumnType("jsonb")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<CustomAppOauthConfig>("OauthConfig")
.HasColumnType("jsonb")
.HasColumnName("oauth_config");
b.Property<CloudFileReferenceObject>("Picture")
.HasColumnType("jsonb")
.HasColumnName("picture");

View File

@@ -5,7 +5,6 @@ using System.Text.Json;
using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Poll;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore;
@@ -396,20 +395,12 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<CustomAppLinks>("Links")
.HasColumnType("jsonb")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<CustomAppOauthConfig>("OauthConfig")
.HasColumnType("jsonb")
.HasColumnName("oauth_config");
b.Property<CloudFileReferenceObject>("Picture")
.HasColumnType("jsonb")
.HasColumnName("picture");

View File

@@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.PageData;
public class PostPageData(
AppDatabase db,
AccountService.AccountServiceClient accounts,
PublisherService pub,
Publisher.PublisherService pub,
PostService ps,
IConfiguration configuration
)

View File

@@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.Poll;
[ApiController]
[Route("/api/polls")]
public class PollController(AppDatabase db, PollService polls, PublisherService pub) : ControllerBase
public class PollController(AppDatabase db, PollService polls, Publisher.PublisherService pub) : ControllerBase
{
[HttpGet("{id:guid}")]
public async Task<ActionResult<PollWithStats>> GetPoll(Guid id)
@@ -85,7 +85,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
.FirstOrDefaultAsync(p => p.Id == id);
if (poll is null) return NotFound("Poll not found");
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, PublisherMemberRole.Viewer))
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Viewer))
return StatusCode(403, "You need to be a viewer to view this poll's feedback.");
var answerQuery = db.PollAnswers
@@ -186,7 +186,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
var publisher = await pub.GetPublisherByName(pubName);
if (publisher is null) return BadRequest("Publisher was not found.");
if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await pub.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You need at least be an editor to create polls as this publisher.");
var poll = new Poll
@@ -231,7 +231,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
if (poll == null) return NotFound("Poll not found");
// Check if user is an editor of the publisher that owns the poll
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, PublisherMemberRole.Editor))
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You need to be at least an editor to update this poll.");
// Update properties if they are provided in the request
@@ -294,7 +294,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
if (poll == null) return NotFound("Poll not found");
// Check if user is an editor of the publisher that owns the poll
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, PublisherMemberRole.Editor))
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You need to be at least an editor to delete this poll.");
// Delete all answers for this poll

View File

@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using PublisherService = DysonNetwork.Sphere.Publisher.PublisherService;
namespace DysonNetwork.Sphere.Post;
@@ -301,13 +302,13 @@ public class PostController(
{
// Use the first personal publisher
publisher = await db.Publishers.FirstOrDefaultAsync(e =>
e.AccountId == accountId && e.Type == PublisherType.Individual);
e.AccountId == accountId && e.Type == Publisher.PublisherType.Individual);
}
else
{
publisher = await pub.GetPublisherByName(pubName);
if (publisher is null) return BadRequest("Publisher was not found.");
if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await pub.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You need at least be an editor to post as this publisher.");
}
@@ -473,14 +474,14 @@ public class PostController(
if (post is null) return NotFound();
var accountId = Guid.Parse(currentUser.Id);
if (!await pub.IsMemberWithRole(post.Publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await pub.IsMemberWithRole(post.Publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You need at least be an editor to edit this publisher's post.");
if (pubName is not null)
{
var publisher = await pub.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await pub.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You need at least be an editor to transfer this post to this publisher.");
post.PublisherId = publisher.Id;
post.Publisher = publisher;
@@ -552,7 +553,8 @@ public class PostController(
.FirstOrDefaultAsync();
if (post is null) return NotFound();
if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id),
Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You need at least be an editor to delete the publisher's post.");
await ps.DeletePostAsync(post);

View File

@@ -161,7 +161,7 @@ public partial class PostService(
{
var sender = post.Publisher;
using var scope = factory.CreateScope();
var pub = scope.ServiceProvider.GetRequiredService<PublisherService>();
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>();
var nty = scope.ServiceProvider.GetRequiredService<PusherService.PusherServiceClient>();
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
try
@@ -455,7 +455,7 @@ public partial class PostService(
_ = Task.Run(async () =>
{
using var scope = factory.CreateScope();
var pub = scope.ServiceProvider.GetRequiredService<PublisherService>();
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>();
var nty = scope.ServiceProvider.GetRequiredService<PusherService.PusherServiceClient>();
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
try

View File

@@ -5,6 +5,7 @@ using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere.Post;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.Protobuf;
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
using Account = DysonNetwork.Pass.Account.Account;
@@ -49,6 +50,49 @@ public class Publisher : ModelBase, IIdentifiedResource
[NotMapped] public Account? Account { get; set; }
public string ResourceIdentifier => $"publisher:{Id}";
public Shared.Proto.Publisher ToProto(AppDatabase db)
{
var p = new Shared.Proto.Publisher()
{
Id = Id.ToString(),
Type = Type == PublisherType.Individual
? Shared.Proto.PublisherType.Individual
: Shared.Proto.PublisherType.Organizational,
Name = Name,
Nick = Nick,
Bio = Bio,
AccountId = AccountId?.ToString() ?? string.Empty,
RealmId = RealmId?.ToString() ?? string.Empty,
CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
if (Picture is not null)
{
p.Picture = new Shared.Proto.CloudFile
{
Id = Picture.Id,
Name = Picture.Name,
MimeType = Picture.MimeType,
Hash = Picture.Hash,
Size = Picture.Size,
};
}
if (Background is not null)
{
p.Background = new Shared.Proto.CloudFile
{
Id = Background.Id,
Name = Background.Name,
MimeType = Background.MimeType,
Hash = Background.Hash,
Size = Background.Size,
};
}
return p;
}
}
public enum PublisherMemberRole
@@ -68,6 +112,25 @@ public class PublisherMember : ModelBase
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
public Instant? JoinedAt { get; set; }
public Shared.Proto.PublisherMember ToProto()
{
return new Shared.Proto.PublisherMember()
{
PublisherId = PublisherId.ToString(),
AccountId = AccountId.ToString(),
Role = Role switch
{
PublisherMemberRole.Owner => Shared.Proto.PublisherMemberRole.Owner,
PublisherMemberRole.Manager => Shared.Proto.PublisherMemberRole.Manager,
PublisherMemberRole.Editor => Shared.Proto.PublisherMemberRole.Editor,
PublisherMemberRole.Viewer => Shared.Proto.PublisherMemberRole.Viewer,
_ => throw new ArgumentOutOfRangeException(nameof(Role), Role, null)
},
JoinedAt = JoinedAt?.ToTimestamp()
};
}
}
public enum PublisherSubscriptionStatus

View File

@@ -0,0 +1,68 @@
using DysonNetwork.Shared.Proto;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Publisher;
public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
: Shared.Proto.PublisherService.PublisherServiceBase
{
public override async Task<GetPublisherResponse> GetPublisher(GetPublisherRequest request,
ServerCallContext context)
{
var p = await service.GetPublisherByName(request.Name);
if (p is null) throw new RpcException(new Status(StatusCode.NotFound, "publisher not found"));
return new GetPublisherResponse { Publisher = p.ToProto(db) };
}
public override async Task<ListPublishersResponse> ListPublishers(ListPublishersRequest request,
ServerCallContext context)
{
IQueryable<Publisher> query = db.Publishers.AsQueryable();
if (!string.IsNullOrWhiteSpace(request.AccountId) && Guid.TryParse(request.AccountId, out var aid))
{
var ids = await db.PublisherMembers.Where(m => m.AccountId == aid).Select(m => m.PublisherId).ToListAsync();
query = query.Where(p => ids.Contains(p.Id));
}
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
{
query = query.Where(p => p.RealmId == rid);
}
var list = await query.Take(request.PageSize > 0 ? request.PageSize : 100).ToListAsync();
var resp = new ListPublishersResponse();
resp.Publishers.AddRange(list.Select(p => p.ToProto(db)));
resp.TotalSize = list.Count;
return resp;
}
public override async Task<ListPublisherMembersResponse> ListPublisherMembers(ListPublisherMembersRequest request,
ServerCallContext context)
{
if (!Guid.TryParse(request.PublisherId, out var pid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
var members = await service.GetPublisherMembers(pid);
var resp = new ListPublisherMembersResponse();
resp.Members.AddRange(members.Select(m => m.ToProto()));
return resp;
}
public override async Task<Google.Protobuf.WellKnownTypes.StringValue> SetPublisherFeatureFlag(
SetPublisherFeatureFlagRequest request, ServerCallContext context)
{
if (!Guid.TryParse(request.PublisherId, out var pid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
await service.SetFeatureFlag(pid, request.Flag);
return new Google.Protobuf.WellKnownTypes.StringValue { Value = request.Flag };
}
public override async Task<HasPublisherFeatureResponse> HasPublisherFeature(HasPublisherFeatureRequest request,
ServerCallContext context)
{
if (!Guid.TryParse(request.PublisherId, out var pid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
var enabled = await service.HasFeature(pid, request.Flag);
return new HasPublisherFeatureResponse { Enabled = enabled };
}
}

View File

@@ -1,6 +1,7 @@
using System.Net;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Sphere.Connection;
using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.HttpOverrides;
using Prometheus;
@@ -29,6 +30,7 @@ public static class ApplicationConfiguration
// Map gRPC services
app.MapGrpcService<WebSocketHandlerGrpc>();
app.MapGrpcService<PublisherServiceGrpc>();
return app;
}

View File

@@ -17,7 +17,6 @@ using System.Threading.RateLimiting;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.GeoIp;
using DysonNetwork.Sphere.WebReader;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Discovery;
using DysonNetwork.Sphere.Poll;
using DysonNetwork.Sphere.Translation;
@@ -169,7 +168,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<WebReaderService>();
services.AddScoped<WebFeedService>();
services.AddScoped<DiscoveryService>();
services.AddScoped<CustomAppService>();
services.AddScoped<PollService>();
var translationProvider = configuration["Translation:Provider"]?.ToLower();

View File

@@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@@ -16,7 +15,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
private async Task<IActionResult> _CheckStickerPackPermissions(
Guid packId,
Account currentUser,
PublisherMemberRole requiredRole
Publisher.PublisherMemberRole requiredRole
)
{
var pack = await db.StickerPacks
@@ -149,7 +148,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
.FirstOrDefaultAsync(m => m.AccountId == accountId && m.PublisherId == pack.PublisherId);
if (member is null)
return StatusCode(403, "You are not a member of this publisher");
if (member.Role < PublisherMemberRole.Editor)
if (member.Role < Publisher.PublisherMemberRole.Editor)
return StatusCode(403, "You need to be at least an editor to update sticker packs");
if (request.Name is not null)
@@ -181,7 +180,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
.FirstOrDefaultAsync(m => m.AccountId == accountId && m.PublisherId == pack.PublisherId);
if (member is null)
return StatusCode(403, "You are not a member of this publisher");
if (member.Role < PublisherMemberRole.Editor)
if (member.Role < Publisher.PublisherMemberRole.Editor)
return StatusCode(403, "You need to be an editor to delete sticker packs");
await st.DeleteStickerPackAsync(pack);
@@ -242,7 +241,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, PublisherMemberRole.Editor);
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, Publisher.PublisherMemberRole.Editor);
if (permissionCheck is not OkResult)
return permissionCheck;
@@ -278,7 +277,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, PublisherMemberRole.Editor);
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, Publisher.PublisherMemberRole.Editor);
if (permissionCheck is not OkResult)
return permissionCheck;
@@ -309,7 +308,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
if (request.ImageId is null)
return BadRequest("Image is required.");
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, PublisherMemberRole.Editor);
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, Publisher.PublisherMemberRole.Editor);
if (permissionCheck is not OkResult)
return permissionCheck;

View File

@@ -9,7 +9,7 @@ namespace DysonNetwork.Sphere.WebReader;
[Authorize]
[ApiController]
[Route("/api/publishers/{pubName}/feeds")]
public class WebFeedController(WebFeedService webFeed, PublisherService ps) : ControllerBase
public class WebFeedController(WebFeedService webFeed, Publisher.PublisherService ps) : ControllerBase
{
public record WebFeedRequest(
[MaxLength(8192)] string? Url,
@@ -53,7 +53,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
if (publisher is null) return NotFound();
var accountId = Guid.Parse(currentUser.Id);
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to create a web feed");
var feed = await webFeed.CreateWebFeedAsync(publisher, request);
@@ -70,7 +70,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
if (publisher is null) return NotFound();
var accountId = Guid.Parse(currentUser.Id);
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to update a web feed");
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
@@ -91,7 +91,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
if (publisher is null) return NotFound();
var accountId = Guid.Parse(currentUser.Id);
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to delete a web feed");
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
@@ -114,7 +114,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
if (publisher is null) return NotFound();
var accountId = Guid.Parse(currentUser.Id);
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to scrape a web feed");
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);

View File

@@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Drive", "Dyson
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Gateway", "DysonNetwork.Gateway\DysonNetwork.Gateway.csproj", "{19EB0086-4049-4B78-91C4-EAC37130A006}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Develop", "DysonNetwork.Develop\DysonNetwork.Develop.csproj", "{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,5 +50,9 @@ Global
{19EB0086-4049-4B78-91C4-EAC37130A006}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19EB0086-4049-4B78-91C4-EAC37130A006}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19EB0086-4049-4B78-91C4-EAC37130A006}.Release|Any CPU.Build.0 = Release|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal