✨ Notification service
This commit is contained in:
parent
3080e273cb
commit
cb7179aa27
41
DysonNetwork.Sphere/Account/Notification.cs
Normal file
41
DysonNetwork.Sphere/Account/Notification.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public class Notification : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string? Title { get; set; }
|
||||
[MaxLength(2048)] public string? Subtitle { get; set; }
|
||||
[MaxLength(4096)] public string? Content { get; set; }
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
|
||||
public int Priority { get; set; } = 10;
|
||||
public Instant? ViewedAt { get; set; }
|
||||
|
||||
public long AccountId { get; set; }
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
}
|
||||
|
||||
public enum NotificationPushProvider
|
||||
{
|
||||
Apple,
|
||||
Google
|
||||
}
|
||||
|
||||
[Index(nameof(DeviceId), IsUnique = true)]
|
||||
[Index(nameof(DeviceToken), IsUnique = true)]
|
||||
public class NotificationPushSubscription : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(4096)] public string DeviceId { get; set; } = null!;
|
||||
[MaxLength(4096)] public string DeviceToken { get; set; } = null!;
|
||||
public NotificationPushProvider Provider { get; set; }
|
||||
public Instant? LastUsedAt { get; set; }
|
||||
|
||||
public long AccountId { get; set; }
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
}
|
182
DysonNetwork.Sphere/Account/NotificationService.cs
Normal file
182
DysonNetwork.Sphere/Account/NotificationService.cs
Normal file
@ -0,0 +1,182 @@
|
||||
using CorePush.Apple;
|
||||
using CorePush.Firebase;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public class NotificationService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
private readonly ILogger<NotificationService> _logger;
|
||||
private readonly FirebaseSender? _fcm;
|
||||
private readonly ApnSender? _apns;
|
||||
|
||||
public NotificationService(
|
||||
AppDatabase db,
|
||||
IConfiguration cfg,
|
||||
IHttpClientFactory clientFactory,
|
||||
ILogger<NotificationService> logger
|
||||
)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
|
||||
var cfgSection = cfg.GetSection("Notifications:Push");
|
||||
|
||||
// Set up the firebase push notification
|
||||
var fcmConfig = cfgSection.GetValue<string>("Google");
|
||||
if (fcmConfig != null)
|
||||
_fcm = new FirebaseSender(File.ReadAllText(fcmConfig), clientFactory.CreateClient());
|
||||
// Set up the apple push notification service
|
||||
var apnsCert = cfgSection.GetValue<string>("Apple:PrivateKey");
|
||||
if (apnsCert != null)
|
||||
_apns = new ApnSender(new ApnSettings
|
||||
{
|
||||
P8PrivateKey = File.ReadAllText(apnsCert),
|
||||
P8PrivateKeyId = cfgSection.GetValue<string>("Apple:PrivateKeyId"),
|
||||
TeamId = cfgSection.GetValue<string>("Apple:TeamId"),
|
||||
AppBundleIdentifier = cfgSection.GetValue<string>("Apple:BundleIdentifier"),
|
||||
ServerType = cfgSection.GetValue<bool>("Production")
|
||||
? ApnServerType.Production
|
||||
: ApnServerType.Development
|
||||
}, clientFactory.CreateClient());
|
||||
}
|
||||
|
||||
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
||||
Account account,
|
||||
NotificationPushProvider provider,
|
||||
string deviceId,
|
||||
string deviceToken
|
||||
)
|
||||
{
|
||||
var existingSubscription = await _db.NotificationPushSubscriptions
|
||||
.Where(s => s.AccountId == account.Id)
|
||||
.Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingSubscription != null)
|
||||
{
|
||||
// Reset these audit fields to renew the lifecycle of this device token
|
||||
existingSubscription.CreatedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
existingSubscription.UpdatedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
_db.Update(existingSubscription);
|
||||
await _db.SaveChangesAsync();
|
||||
return existingSubscription;
|
||||
}
|
||||
|
||||
var subscription = new NotificationPushSubscription
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceToken = deviceToken,
|
||||
Provider = provider,
|
||||
Account = account,
|
||||
AccountId = account.Id,
|
||||
};
|
||||
|
||||
_db.Add(subscription);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public async Task<Notification> SendNotification(
|
||||
Account account,
|
||||
string? title = null,
|
||||
string? subtitle = null,
|
||||
string? content = null,
|
||||
Dictionary<string, object>? meta = null,
|
||||
bool isSilent = false
|
||||
)
|
||||
{
|
||||
if (title is null && subtitle is null && content is null)
|
||||
{
|
||||
throw new ArgumentException("Unable to send notification that completely empty.");
|
||||
}
|
||||
|
||||
var notification = new Notification
|
||||
{
|
||||
Title = title,
|
||||
Subtitle = subtitle,
|
||||
Content = content,
|
||||
Meta = meta,
|
||||
Account = account,
|
||||
AccountId = account.Id,
|
||||
};
|
||||
|
||||
_db.Add(notification);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
#pragma warning disable CS4014
|
||||
if (!isSilent) DeliveryNotification(notification);
|
||||
#pragma warning restore CS4014
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
public async Task DeliveryNotification(Notification notification)
|
||||
{
|
||||
// TODO send websocket
|
||||
|
||||
// Pushing the notification
|
||||
var subscribers = await _db.NotificationPushSubscriptions
|
||||
.Where(s => s.AccountId == notification.AccountId)
|
||||
.ToListAsync();
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var subscriber in subscribers)
|
||||
{
|
||||
tasks.Add(_PushSingleNotification(notification, subscriber));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task _PushSingleNotification(Notification notification, NotificationPushSubscription subscription)
|
||||
{
|
||||
switch (subscription.Provider)
|
||||
{
|
||||
case NotificationPushProvider.Google:
|
||||
if (_fcm == null)
|
||||
throw new InvalidOperationException("The firebase cloud messaging is not initialized.");
|
||||
await _fcm.SendAsync(new
|
||||
{
|
||||
message = new
|
||||
{
|
||||
token = subscription.DeviceToken,
|
||||
notification = new
|
||||
{
|
||||
title = notification.Title,
|
||||
body = string.Join("\n", notification.Subtitle, notification.Content),
|
||||
},
|
||||
data = notification.Meta
|
||||
}
|
||||
});
|
||||
break;
|
||||
case NotificationPushProvider.Apple:
|
||||
if (_apns == null)
|
||||
throw new InvalidOperationException("The apple notification push service is not initialized.");
|
||||
await _apns.SendAsync(new
|
||||
{
|
||||
apns = new
|
||||
{
|
||||
alert = new
|
||||
{
|
||||
title = notification.Title,
|
||||
subtitle = notification.Subtitle,
|
||||
content = notification.Content,
|
||||
}
|
||||
},
|
||||
meta = notification.Meta,
|
||||
},
|
||||
deviceToken: subscription.DeviceToken,
|
||||
apnsId: notification.Id.ToString(),
|
||||
apnsPriority: notification.Priority,
|
||||
apnPushType: ApnPushType.Alert
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Provider not supported: {subscription.Provider}");
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,8 @@ public class AppDatabase(
|
||||
public DbSet<Account.Relationship> AccountRelationships { get; set; }
|
||||
public DbSet<Auth.Session> AuthSessions { get; set; }
|
||||
public DbSet<Auth.Challenge> AuthChallenges { get; set; }
|
||||
public DbSet<Account.Notification> Notifications { get; set; }
|
||||
public DbSet<Account.NotificationPushSubscription> NotificationPushSubscriptions { get; set; }
|
||||
public DbSet<Storage.CloudFile> Files { get; set; }
|
||||
public DbSet<Post.Publisher> Publishers { get; set; }
|
||||
public DbSet<Post.PublisherMember> PublisherMembers { get; set; }
|
||||
|
@ -29,6 +29,7 @@ public class Challenge : ModelBase
|
||||
[MaxLength(256)] public string? DeviceId { get; set; }
|
||||
[MaxLength(1024)] public string? Nonce { get; set; }
|
||||
|
||||
public long AccountId { get; set; }
|
||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||
|
||||
public Challenge Normalize()
|
||||
|
@ -13,6 +13,7 @@
|
||||
<PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" />
|
||||
<PackageReference Include="Casbin.NET" Version="2.12.0" />
|
||||
<PackageReference Include="Casbin.NET.Adapter.EFCore" Version="2.5.0" />
|
||||
<PackageReference Include="CorePush" Version="4.3.0" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
|
1406
DysonNetwork.Sphere/Migrations/20250427154356_AddNotification.Designer.cs
generated
Normal file
1406
DysonNetwork.Sphere/Migrations/20250427154356_AddNotification.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
201
DysonNetwork.Sphere/Migrations/20250427154356_AddNotification.cs
Normal file
201
DysonNetwork.Sphere/Migrations/20250427154356_AddNotification.cs
Normal file
@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotification : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "picture_id",
|
||||
table: "publishers",
|
||||
type: "character varying(128)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "background_id",
|
||||
table: "publishers",
|
||||
type: "character varying(128)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "id",
|
||||
table: "files",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "expired_at",
|
||||
table: "files",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "picture_id",
|
||||
table: "account_profiles",
|
||||
type: "character varying(128)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "background_id",
|
||||
table: "account_profiles",
|
||||
type: "character varying(128)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "notification_push_subscriptions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
device_id = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
device_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
provider = table.Column<int>(type: "integer", nullable: false),
|
||||
last_used_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
account_id = table.Column<long>(type: "bigint", 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),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_notification_push_subscriptions", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_notification_push_subscriptions_accounts_account_id",
|
||||
column: x => x.account_id,
|
||||
principalTable: "accounts",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "notifications",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
title = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
subtitle = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
content = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
|
||||
priority = table.Column<int>(type: "integer", nullable: false),
|
||||
viewed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
account_id = table.Column<long>(type: "bigint", 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),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_notifications", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_notifications_accounts_account_id",
|
||||
column: x => x.account_id,
|
||||
principalTable: "accounts",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notification_push_subscriptions_account_id",
|
||||
table: "notification_push_subscriptions",
|
||||
column: "account_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notification_push_subscriptions_device_id",
|
||||
table: "notification_push_subscriptions",
|
||||
column: "device_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notification_push_subscriptions_device_token",
|
||||
table: "notification_push_subscriptions",
|
||||
column: "device_token",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notifications_account_id",
|
||||
table: "notifications",
|
||||
column: "account_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "notification_push_subscriptions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "notifications");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "expired_at",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "picture_id",
|
||||
table: "publishers",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(128)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "background_id",
|
||||
table: "publishers",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(128)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "id",
|
||||
table: "files",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(128)",
|
||||
oldMaxLength: 128);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "picture_id",
|
||||
table: "account_profiles",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(128)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "background_id",
|
||||
table: "account_profiles",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(128)",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
@ -167,6 +167,125 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.ToTable("account_contacts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AccountId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("content");
|
||||
|
||||
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<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)")
|
||||
.HasColumnName("subtitle");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("ViewedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("viewed_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifications");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.HasDatabaseName("ix_notifications_account_id");
|
||||
|
||||
b.ToTable("notifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.NotificationPushSubscription", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AccountId")
|
||||
.HasColumnType("bigint")
|
||||
.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>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("device_id");
|
||||
|
||||
b.Property<string>("DeviceToken")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("device_token");
|
||||
|
||||
b.Property<Instant?>("LastUsedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_used_at");
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("provider");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notification_push_subscriptions");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.HasDatabaseName("ix_notification_push_subscriptions_account_id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_notification_push_subscriptions_device_id");
|
||||
|
||||
b.HasIndex("DeviceToken")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_notification_push_subscriptions_device_token");
|
||||
|
||||
b.ToTable("notification_push_subscriptions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@ -174,7 +293,7 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("BackgroundId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("background_id");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
@ -206,7 +325,7 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnName("middle_name");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("picture_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
@ -692,7 +811,7 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<string>("BackgroundId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("background_id");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
@ -721,7 +840,7 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnName("nick");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("picture_id");
|
||||
|
||||
b.Property<int>("PublisherType")
|
||||
@ -793,7 +912,8 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AccountId")
|
||||
@ -813,6 +933,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
@ -955,6 +1079,30 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Notification", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notifications_accounts_account_id");
|
||||
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.NotificationPushSubscription", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notification_push_subscriptions_accounts_account_id");
|
||||
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background")
|
||||
|
@ -27,6 +27,7 @@ public class PostController(AppDatabase db, PostService ps, IEnforcer enforcer)
|
||||
.Include(e => e.Attachments)
|
||||
.Include(e => e.Categories)
|
||||
.Include(e => e.Tags)
|
||||
.Where(e => e.RepliedPostId == null)
|
||||
.FilterWithVisibility(currentUser, isListing: true)
|
||||
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
|
||||
.Skip(offset)
|
||||
@ -110,6 +111,8 @@ public class PostController(AppDatabase db, PostService ps, IEnforcer enforcer)
|
||||
[MaxLength(32)] public List<string>? Attachments { get; set; }
|
||||
public Dictionary<string, object>? Meta { get; set; }
|
||||
public Instant? PublishedAt { get; set; }
|
||||
public long? RepliedPostId { get; set; }
|
||||
public long? ForwardedPostId { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -155,6 +158,22 @@ public class PostController(AppDatabase db, PostService ps, IEnforcer enforcer)
|
||||
Publisher = publisher,
|
||||
};
|
||||
|
||||
if (request.RepliedPostId is not null)
|
||||
{
|
||||
var repliedPost = await db.Posts.FindAsync(request.RepliedPostId.Value);
|
||||
if (repliedPost is null) return BadRequest("Post replying to was not found.");
|
||||
post.RepliedPost = repliedPost;
|
||||
post.RepliedPostId = repliedPost.Id;
|
||||
}
|
||||
|
||||
if (request.ForwardedPostId is not null)
|
||||
{
|
||||
var forwardedPost = await db.Posts.FindAsync(request.ForwardedPostId.Value);
|
||||
if (forwardedPost is null) return BadRequest("Forwarded post was not found.");
|
||||
post.ForwardedPost = forwardedPost;
|
||||
post.ForwardedPostId = forwardedPost.Id;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
post = await ps.PostAsync(
|
||||
@ -226,6 +245,7 @@ public class PostController(AppDatabase db, PostService ps, IEnforcer enforcer)
|
||||
|
||||
var post = await db.Posts
|
||||
.Where(e => e.Id == id)
|
||||
.Include(e => e.Publisher)
|
||||
.Include(e => e.Attachments)
|
||||
.FirstOrDefaultAsync();
|
||||
if (post is null) return NotFound();
|
||||
|
@ -130,6 +130,7 @@ builder.Services.AddSwaggerGen(options =>
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddScoped<AccountService>();
|
||||
builder.Services.AddScoped<NotificationService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<FileService>();
|
||||
builder.Services.AddScoped<PublisherService>();
|
||||
|
@ -5,7 +5,7 @@ using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
|
||||
public class RemoteStorageConfig
|
||||
public abstract class RemoteStorageConfig
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
@ -22,7 +22,7 @@ public class RemoteStorageConfig
|
||||
|
||||
public class CloudFile : ModelBase
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
[MaxLength(128)] public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
[MaxLength(1024)] public string Name { get; set; } = string.Empty;
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, object>? FileMeta { get; set; } = null!;
|
||||
@ -32,6 +32,7 @@ public class CloudFile : ModelBase
|
||||
[MaxLength(256)] public string? Hash { get; set; }
|
||||
public long Size { get; set; }
|
||||
public Instant? UploadedAt { get; set; }
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
[MaxLength(128)] public string? UploadedTo { get; set; }
|
||||
|
||||
// Metrics
|
||||
|
@ -249,9 +249,12 @@ public class CloudFileUnusedRecyclingJob(AppDatabase db, FileService fs, ILogger
|
||||
logger.LogInformation("Deleting unused cloud files...");
|
||||
|
||||
var cutoff = SystemClock.Instance.GetCurrentInstant() - Duration.FromHours(1);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var files = db.Files
|
||||
.Where(f => f.UsedCount == 0)
|
||||
.Where(f => f.CreatedAt < cutoff)
|
||||
.Where(f =>
|
||||
(f.ExpiredAt == null && f.UsedCount == 0 && f.CreatedAt < cutoff) ||
|
||||
(f.ExpiredAt != null && f.ExpiredAt >= now)
|
||||
)
|
||||
.ToList();
|
||||
|
||||
logger.LogInformation($"Deleting {files.Count} unused cloud files...");
|
||||
|
@ -49,5 +49,17 @@
|
||||
"Provider": "recaptcha",
|
||||
"ApiKey": "6LfIzSArAAAAAN413MtycDcPlKa636knBSAhbzj-",
|
||||
"ApiSecret": ""
|
||||
},
|
||||
"Notifications": {
|
||||
"Push": {
|
||||
"Production": true,
|
||||
"Google": "path/to/config.json",
|
||||
"Apple": {
|
||||
"PrivateKey": "path/to/cert.p8",
|
||||
"PrivateKeyId": "",
|
||||
"TeamId": "",
|
||||
"BundleIdentifier": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003Fc5_003F2a1973a9_003FApnSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationMiddleware_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003F2f_003F7ab1cc57_003FAuthenticationMiddleware_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthorizationAppBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2ff26593f91746d7a53418a46dc419d1f200_003F4b_003F56550da2_003FAuthorizationAppBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
Loading…
x
Reference in New Issue
Block a user