[backend/masto-client] Add push notification support (ISH-182)
This commit is contained in:
parent
3ab36f7d70
commit
a623b870cd
20 changed files with 12556 additions and 37 deletions
|
@ -1,6 +1,7 @@
|
|||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -30,7 +31,7 @@ public class AuthController(DatabaseContext db) : ControllerBase
|
|||
|
||||
var res = new AuthSchemas.VerifyAppCredentialsResponse
|
||||
{
|
||||
App = token.App, VapidKey = null //FIXME
|
||||
App = token.App, VapidKey = Constants.VapidPublicKey
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
|
@ -80,7 +81,7 @@ public class AuthController(DatabaseContext db) : ControllerBase
|
|||
|
||||
var res = new AuthSchemas.RegisterAppResponse
|
||||
{
|
||||
App = app, VapidKey = null //FIXME
|
||||
App = app, VapidKey = Constants.VapidPublicKey
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
|
|
|
@ -64,12 +64,12 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
[Authorize("read:notifications")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<NotificationEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNotification(string id)
|
||||
public async Task<IActionResult> GetNotification(long id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notification = await db.Notifications
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => p.Notifiee == user && p.Id == id)
|
||||
.Where(p => p.Notifiee == user && p.MastoId == id)
|
||||
.EnsureNoteVisibilityFor(p => p.Note, user)
|
||||
.PrecomputeNoteVisibilities(user)
|
||||
.FirstOrDefaultAsync() ??
|
||||
|
|
171
Iceshrimp.Backend/Controllers/Mastodon/PushController.cs
Normal file
171
Iceshrimp.Backend/Controllers/Mastodon/PushController.cs
Normal file
|
@ -0,0 +1,171 @@
|
|||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||
|
||||
[MastodonApiController]
|
||||
[Route("/api/v1/push/subscription")]
|
||||
[Authenticate]
|
||||
[Authorize("push")]
|
||||
[EnableRateLimiting("sliding")]
|
||||
[EnableCors("mastodon")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
public class PushController(DatabaseContext db) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PushSchemas.PushSubscription))]
|
||||
public async Task<IActionResult> RegisterSubscription([FromHybrid] PushSchemas.RegisterPushRequest request)
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var pushSubscription = await db.PushSubscriptions.FirstOrDefaultAsync(p => p.OauthToken == token);
|
||||
if (pushSubscription == null)
|
||||
{
|
||||
pushSubscription = new PushSubscription
|
||||
{
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Endpoint = request.Subscription.Endpoint,
|
||||
User = token.User,
|
||||
OauthToken = token,
|
||||
PublicKey = request.Subscription.Keys.PublicKey,
|
||||
AuthSecret = request.Subscription.Keys.AuthSecret,
|
||||
Types = GetTypes(request.Data.Alerts),
|
||||
Policy = GetPolicy(request.Data.Policy)
|
||||
};
|
||||
|
||||
await db.AddAsync(pushSubscription);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var res = RenderSubscription(pushSubscription);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PushSchemas.PushSubscription))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> EditSubscription([FromHybrid] PushSchemas.EditPushRequest request)
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var pushSubscription = await db.PushSubscriptions.FirstOrDefaultAsync(p => p.OauthToken == token) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
pushSubscription.Types = GetTypes(request.Data.Alerts);
|
||||
pushSubscription.Policy = GetPolicy(request.Data.Policy);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = RenderSubscription(pushSubscription);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PushSchemas.PushSubscription))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetSubscription()
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var pushSubscription = await db.PushSubscriptions.FirstOrDefaultAsync(p => p.OauthToken == token) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
var res = RenderSubscription(pushSubscription);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
public async Task<IActionResult> DeleteSubscription()
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
await db.PushSubscriptions.Where(p => p.OauthToken == token).ExecuteDeleteAsync();
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
|
||||
private PushSubscription.PushPolicy GetPolicy(string policy)
|
||||
{
|
||||
return policy switch
|
||||
{
|
||||
"all" => PushSubscription.PushPolicy.All,
|
||||
"follower" => PushSubscription.PushPolicy.Follower,
|
||||
"followed" => PushSubscription.PushPolicy.Followed,
|
||||
"none" => PushSubscription.PushPolicy.None,
|
||||
_ => throw GracefulException.BadRequest("Unknown push policy")
|
||||
};
|
||||
}
|
||||
|
||||
private string GetPolicyString(PushSubscription.PushPolicy policy)
|
||||
{
|
||||
return policy switch
|
||||
{
|
||||
PushSubscription.PushPolicy.All => "all",
|
||||
PushSubscription.PushPolicy.Follower => "follower",
|
||||
PushSubscription.PushPolicy.Followed => "followed",
|
||||
PushSubscription.PushPolicy.None => "none",
|
||||
_ => throw GracefulException.BadRequest("Unknown push policy")
|
||||
};
|
||||
}
|
||||
|
||||
private List<string> GetTypes(PushSchemas.Alerts alerts)
|
||||
{
|
||||
List<string> types = [];
|
||||
|
||||
if (alerts.Favourite)
|
||||
types.Add("favourite");
|
||||
if (alerts.Follow)
|
||||
types.Add("follow");
|
||||
if (alerts.Mention)
|
||||
types.Add("mention");
|
||||
if (alerts.Poll)
|
||||
types.Add("poll");
|
||||
if (alerts.Reblog)
|
||||
types.Add("reblog");
|
||||
if (alerts.Status)
|
||||
types.Add("status");
|
||||
if (alerts.Update)
|
||||
types.Add("update");
|
||||
if (alerts.FollowRequest)
|
||||
types.Add("follow_request");
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
private PushSchemas.PushSubscription RenderSubscription(PushSubscription sub)
|
||||
{
|
||||
return new PushSchemas.PushSubscription
|
||||
{
|
||||
Id = sub.Id,
|
||||
Endpoint = sub.Endpoint,
|
||||
ServerKey = Constants.VapidPublicKey,
|
||||
Policy = GetPolicyString(sub.Policy),
|
||||
Alerts = new PushSchemas.Alerts
|
||||
{
|
||||
Favourite = sub.Types.Contains("favourite"),
|
||||
Follow = sub.Types.Contains("follow"),
|
||||
Mention = sub.Types.Contains("mention"),
|
||||
Poll = sub.Types.Contains("poll"),
|
||||
Reblog = sub.Types.Contains("reblog"),
|
||||
Status = sub.Types.Contains("status"),
|
||||
Update = sub.Types.Contains("update"),
|
||||
FollowRequest = sub.Types.Contains("follow_request")
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRe
|
|||
|
||||
var res = new NotificationEntity
|
||||
{
|
||||
Id = notification.Id,
|
||||
Id = notification.MastoId.ToString(),
|
||||
Type = NotificationEntity.EncodeType(notification.Type),
|
||||
Note = note,
|
||||
Notifier = notifier,
|
||||
|
|
102
Iceshrimp.Backend/Controllers/Mastodon/Schemas/PushSchemas.cs
Normal file
102
Iceshrimp.Backend/Controllers/Mastodon/Schemas/PushSchemas.cs
Normal file
|
@ -0,0 +1,102 @@
|
|||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
|
||||
using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
|
||||
public abstract class PushSchemas
|
||||
{
|
||||
public class PushSubscription
|
||||
{
|
||||
[J("id")] public required string Id { get; set; }
|
||||
[J("endpoint")] public required string Endpoint { get; set; }
|
||||
[J("server_key")] public required string ServerKey { get; set; }
|
||||
[J("alerts")] public required Alerts Alerts { get; set; }
|
||||
[J("policy")] public required string Policy { get; set; }
|
||||
}
|
||||
|
||||
public class Alerts
|
||||
{
|
||||
[J("mention")] [B(Name = "mention")] public bool Mention { get; set; } = false;
|
||||
[J("status")] [B(Name = "status")] public bool Status { get; set; } = false;
|
||||
[J("reblog")] [B(Name = "reblog")] public bool Reblog { get; set; } = false;
|
||||
[J("follow")] [B(Name = "follow")] public bool Follow { get; set; } = false;
|
||||
|
||||
[J("follow_request")]
|
||||
[B(Name = "follow_request")]
|
||||
public bool FollowRequest { get; set; } = false;
|
||||
|
||||
[J("favourite")]
|
||||
[B(Name = "favourite")]
|
||||
public bool Favourite { get; set; } = false;
|
||||
|
||||
[J("poll")] [B(Name = "poll")] public bool Poll { get; set; } = false;
|
||||
[J("update")] [B(Name = "update")] public bool Update { get; set; } = false;
|
||||
}
|
||||
|
||||
public class RegisterPushRequest
|
||||
{
|
||||
[J("subscription")]
|
||||
[JR]
|
||||
[B(Name = "subscription")]
|
||||
public required Subscription Subscription { get; set; }
|
||||
|
||||
[J("data")] [B(Name = "data")] public RegisterPushRequestData Data { get; set; } = new();
|
||||
}
|
||||
|
||||
public class RegisterPushRequestData
|
||||
{
|
||||
[J("alerts")] [B(Name = "alerts")] public Alerts Alerts { get; set; } = new();
|
||||
[J("policy")] [B(Name = "policy")] public string Policy { get; set; } = "all";
|
||||
}
|
||||
|
||||
public class EditPushRequest
|
||||
{
|
||||
[J("policy")]
|
||||
[B(Name = "policy")]
|
||||
public string Policy
|
||||
{
|
||||
get => Data.Policy;
|
||||
set => Data.Policy = value;
|
||||
}
|
||||
|
||||
[J("data")] [B(Name = "data")] public RegisterPushRequestData Data { get; set; } = new();
|
||||
}
|
||||
|
||||
public class EditPushRequestData
|
||||
{
|
||||
[J("alerts")] [B(Name = "alerts")] public Alerts Alerts { get; set; } = new();
|
||||
[J("policy")] [B(Name = "policy")] public string Policy { get; set; } = "all";
|
||||
}
|
||||
|
||||
public class Subscription
|
||||
{
|
||||
[J("endpoint")]
|
||||
[JR]
|
||||
[B(Name = "endpoint")]
|
||||
public required string Endpoint { get; set; }
|
||||
|
||||
[J("keys")] [JR] [B(Name = "keys")] public required Keys Keys { get; set; }
|
||||
}
|
||||
|
||||
public class Keys
|
||||
{
|
||||
[J("p256dh")]
|
||||
[JR]
|
||||
[B(Name = "p256dh")]
|
||||
public required string PublicKey { get; set; }
|
||||
|
||||
[J("auth")] [JR] [B(Name = "auth")] public required string AuthSecret { get; set; }
|
||||
}
|
||||
|
||||
public class PushNotification
|
||||
{
|
||||
[J("access_token")] public required string AccessToken { get; set; }
|
||||
[J("notification_id")] public required long NotificationId { get; set; }
|
||||
[J("notification_type")] public required string NotificationType { get; set; }
|
||||
[J("icon")] public required string IconUrl { get; set; }
|
||||
[J("title")] public required string Title { get; set; }
|
||||
[J("body")] public required string Body { get; set; }
|
||||
[J("preferred_locale")] public string PreferredLocale => "en";
|
||||
}
|
||||
}
|
|
@ -11,6 +11,12 @@ public static class Constants
|
|||
public const string MisskeyNs = "https://misskey-hub.net/ns";
|
||||
public static readonly string[] SystemUsers = ["instance.actor", "relay.actor"];
|
||||
|
||||
// TODO: reference runtime keys, these are just for initial testing
|
||||
public const string VapidPrivateKey = "qmVGBbXSNVCmcZewcDr04MP7t1gvOFc2aQcThx3rD1A";
|
||||
|
||||
public const string VapidPublicKey =
|
||||
"BEoyQxKBnLn8N1eLmh1va4Al7Azh2LJrfsp7w-PYb4sNSPG1C_SClRUpqw59dwQas3y8yEnFPE7MyGsMg1SItmE";
|
||||
|
||||
public static readonly string[] BrowserSafeMimeTypes =
|
||||
[
|
||||
"image/png",
|
||||
|
|
|
@ -66,6 +66,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
public virtual DbSet<RenoteMuting> RenoteMutings { get; init; } = null!;
|
||||
public virtual DbSet<Session> Sessions { get; init; } = null!;
|
||||
public virtual DbSet<SwSubscription> SwSubscriptions { get; init; } = null!;
|
||||
public virtual DbSet<PushSubscription> PushSubscriptions { get; init; } = null!;
|
||||
public virtual DbSet<UsedUsername> UsedUsernames { get; init; } = null!;
|
||||
public virtual DbSet<User> Users { get; init; } = null!;
|
||||
public virtual DbSet<UserGroup> UserGroups { get; init; } = null!;
|
||||
|
@ -105,6 +106,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
dataSourceBuilder.MapEnum<Relay.RelayStatus>();
|
||||
dataSourceBuilder.MapEnum<UserProfile.UserProfileFFVisibility>();
|
||||
dataSourceBuilder.MapEnum<Marker.MarkerType>();
|
||||
dataSourceBuilder.MapEnum<PushSubscription.PushPolicy>();
|
||||
|
||||
dataSourceBuilder.EnableDynamicJson();
|
||||
|
||||
|
@ -128,6 +130,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
.HasPostgresEnum<Relay.RelayStatus>()
|
||||
.HasPostgresEnum<UserProfile.UserProfileFFVisibility>()
|
||||
.HasPostgresEnum<Marker.MarkerType>()
|
||||
.HasPostgresEnum<PushSubscription.PushPolicy>()
|
||||
.HasPostgresExtension("pg_trgm");
|
||||
|
||||
modelBuilder
|
||||
|
@ -946,6 +949,20 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PushSubscription>(entity =>
|
||||
{
|
||||
entity.Property(e => e.Types).HasDefaultValueSql("'{}'::character varying[]");
|
||||
entity.Property(e => e.Policy).HasDefaultValue(PushSubscription.PushPolicy.All);
|
||||
|
||||
entity.HasOne(d => d.User)
|
||||
.WithMany(p => p.PushSubscriptions)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(d => d.OauthToken)
|
||||
.WithOne(p => p.PushSubscription)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UsedUsername>();
|
||||
|
||||
modelBuilder.Entity<User>(entity =>
|
||||
|
|
5868
Iceshrimp.Backend/Core/Database/Migrations/20240315095854_AddPushSubscriptionTable.Designer.cs
generated
Normal file
5868
Iceshrimp.Backend/Core/Database/Migrations/20240315095854_AddPushSubscriptionTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPushSubscriptionTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "masto_id",
|
||||
table: "notification",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "push_subscription",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
createdAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
userId = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
oauthTokenId = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
endpoint = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
publickey = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_push_subscription", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_push_subscription_oauth_token_oauthTokenId",
|
||||
column: x => x.oauthTokenId,
|
||||
principalTable: "oauth_token",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_push_subscription_user_userId",
|
||||
column: x => x.userId,
|
||||
principalTable: "user",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_push_subscription_oauthTokenId",
|
||||
table: "push_subscription",
|
||||
column: "oauthTokenId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_push_subscription_userId",
|
||||
table: "push_subscription",
|
||||
column: "userId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "push_subscription");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "masto_id",
|
||||
table: "notification");
|
||||
}
|
||||
}
|
||||
}
|
5882
Iceshrimp.Backend/Core/Database/Migrations/20240315180229_AddPushSubscriptionColumns.Designer.cs
generated
Normal file
5882
Iceshrimp.Backend/Core/Database/Migrations/20240315180229_AddPushSubscriptionColumns.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,80 @@
|
|||
using System.Collections.Generic;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPushSubscriptionColumns : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.Annotation("Npgsql:Enum:marker_type_enum", "home,notifications")
|
||||
.Annotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.Annotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,like,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app,edit,bite")
|
||||
.Annotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.Annotation("Npgsql:Enum:push_subscription_policy_enum", "all,followed,follower,none")
|
||||
.Annotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.Annotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.Annotation("Npgsql:PostgresExtension:pg_trgm", ",,")
|
||||
.OldAnnotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.OldAnnotation("Npgsql:Enum:marker_type_enum", "home,notifications")
|
||||
.OldAnnotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,like,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app,edit,bite")
|
||||
.OldAnnotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.OldAnnotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.OldAnnotation("Npgsql:PostgresExtension:pg_trgm", ",,");
|
||||
|
||||
migrationBuilder.AddColumn<PushSubscription.PushPolicy>(
|
||||
name: "policy",
|
||||
table: "push_subscription",
|
||||
type: "push_subscription_policy_enum",
|
||||
nullable: false,
|
||||
defaultValue: PushSubscription.PushPolicy.All);
|
||||
|
||||
migrationBuilder.AddColumn<List<string>>(
|
||||
name: "types",
|
||||
table: "push_subscription",
|
||||
type: "character varying(32)[]",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'::character varying[]");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "policy",
|
||||
table: "push_subscription");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "types",
|
||||
table: "push_subscription");
|
||||
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.Annotation("Npgsql:Enum:marker_type_enum", "home,notifications")
|
||||
.Annotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.Annotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,like,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app,edit,bite")
|
||||
.Annotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.Annotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.Annotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.Annotation("Npgsql:PostgresExtension:pg_trgm", ",,")
|
||||
.OldAnnotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.OldAnnotation("Npgsql:Enum:marker_type_enum", "home,notifications")
|
||||
.OldAnnotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,like,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app,edit,bite")
|
||||
.OldAnnotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:push_subscription_policy_enum", "all,followed,follower,none")
|
||||
.OldAnnotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.OldAnnotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.OldAnnotation("Npgsql:PostgresExtension:pg_trgm", ",,");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "note_visibility_enum", new[] { "public", "home", "followers", "specified" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type_enum", new[] { "follow", "mention", "reply", "renote", "quote", "like", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "edit", "bite" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "page_visibility_enum", new[] { "public", "followers", "specified" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "push_subscription_policy_enum", new[] { "all", "followed", "follower", "none" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "relay_status_enum", new[] { "requesting", "accepted", "rejected" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_profile_ffvisibility_enum", new[] { "public", "followers", "private" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
|
||||
|
@ -2755,6 +2756,13 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
.HasColumnName("isRead")
|
||||
.HasComment("Whether the notification was read.");
|
||||
|
||||
b.Property<long>("MastoId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("masto_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("MastoId"));
|
||||
|
||||
b.Property<string>("NoteId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
|
@ -3309,6 +3317,70 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
b.ToTable("promo_read");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PushSubscription", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AuthSecret")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("auth");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("createdAt");
|
||||
|
||||
b.Property<string>("Endpoint")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("endpoint");
|
||||
|
||||
b.Property<string>("OauthTokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("oauthTokenId");
|
||||
|
||||
b.Property<PushSubscription.PushPolicy>("Policy")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("push_subscription_policy_enum")
|
||||
.HasDefaultValue(PushSubscription.PushPolicy.All)
|
||||
.HasColumnName("policy");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("publickey");
|
||||
|
||||
b.Property<List<string>>("Types")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("character varying(32)[]")
|
||||
.HasColumnName("types")
|
||||
.HasDefaultValueSql("'{}'::character varying[]");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("userId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OauthTokenId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("push_subscription");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistrationInvite", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
@ -3510,7 +3582,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
b.Property<string>("AuthSecret")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
|
@ -3526,7 +3598,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("endpoint");
|
||||
|
||||
b.Property<string>("Publickey")
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
|
@ -5297,6 +5369,25 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PushSubscription", b =>
|
||||
{
|
||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.OauthToken", "OauthToken")
|
||||
.WithOne("PushSubscription")
|
||||
.HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.PushSubscription", "OauthTokenId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User")
|
||||
.WithMany("PushSubscriptions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("OauthToken");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistryItem", b =>
|
||||
{
|
||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User")
|
||||
|
@ -5626,6 +5717,11 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
b.Navigation("OauthTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.OauthToken", b =>
|
||||
{
|
||||
b.Navigation("PushSubscription");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Page", b =>
|
||||
{
|
||||
b.Navigation("PageLikes");
|
||||
|
@ -5715,6 +5811,8 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
|
||||
b.Navigation("PromoReads");
|
||||
|
||||
b.Navigation("PushSubscriptions");
|
||||
|
||||
b.Navigation("RegistryItems");
|
||||
|
||||
b.Navigation("RenoteMutingMutees");
|
||||
|
|
|
@ -122,6 +122,10 @@ public class Notification : IEntity
|
|||
[StringLength(32)]
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
[Column("masto_id")]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long MastoId { get; set; }
|
||||
|
||||
public Notification WithPrecomputedNoteVisibilities(bool reply, bool renote)
|
||||
{
|
||||
Note = Note?.WithPrecomputedVisibilities(reply, renote);
|
||||
|
|
|
@ -65,6 +65,9 @@ public class OauthToken
|
|||
[InverseProperty(nameof(Tables.User.OauthTokens))]
|
||||
public virtual User User { get; set; } = null!;
|
||||
|
||||
[InverseProperty(nameof(Tables.PushSubscription.OauthToken))]
|
||||
public virtual PushSubscription? PushSubscription { get; set; }
|
||||
|
||||
[Column("supportsHtmlFormatting")] public bool SupportsHtmlFormatting { get; set; }
|
||||
[Column("autoDetectQuotes")] public bool AutoDetectQuotes { get; set; }
|
||||
|
||||
|
|
57
Iceshrimp.Backend/Core/Database/Tables/PushSubscription.cs
Normal file
57
Iceshrimp.Backend/Core/Database/Tables/PushSubscription.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NpgsqlTypes;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||
|
||||
[Table("push_subscription")]
|
||||
[Index("UserId")]
|
||||
[Index("OauthTokenId", IsUnique = true)]
|
||||
public class PushSubscription
|
||||
{
|
||||
[PgName("push_subscription_policy_enum")]
|
||||
public enum PushPolicy
|
||||
{
|
||||
[PgName("all")] All,
|
||||
[PgName("followed")] Followed,
|
||||
[PgName("follower")] Follower,
|
||||
[PgName("none")] None,
|
||||
}
|
||||
|
||||
[Key]
|
||||
[Column("id")]
|
||||
[StringLength(32)]
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
[Column("createdAt")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
[Column("userId")] [StringLength(32)] public string UserId { get; set; } = null!;
|
||||
|
||||
[Column("oauthTokenId")]
|
||||
[StringLength(32)]
|
||||
public string OauthTokenId { get; set; } = null!;
|
||||
|
||||
[Column("endpoint")]
|
||||
[StringLength(512)]
|
||||
public string Endpoint { get; set; } = null!;
|
||||
|
||||
[Column("auth")] [StringLength(256)] public string AuthSecret { get; set; } = null!;
|
||||
|
||||
[Column("publickey")]
|
||||
[StringLength(128)]
|
||||
public string PublicKey { get; set; } = null!;
|
||||
|
||||
[Column("types", TypeName = "character varying(32)[]")]
|
||||
public List<string> Types = null!;
|
||||
|
||||
[Column("policy")] public PushPolicy Policy { get; set; }
|
||||
|
||||
[ForeignKey("UserId")]
|
||||
[InverseProperty(nameof(Tables.User.PushSubscriptions))]
|
||||
public virtual User User { get; set; } = null!;
|
||||
|
||||
[ForeignKey("OauthTokenId")]
|
||||
[InverseProperty(nameof(Tables.OauthToken.PushSubscription))]
|
||||
public virtual OauthToken OauthToken { get; set; } = null!;
|
||||
}
|
|
@ -21,11 +21,11 @@ public class SwSubscription
|
|||
[StringLength(512)]
|
||||
public string Endpoint { get; set; } = null!;
|
||||
|
||||
[Column("auth")] [StringLength(256)] public string Auth { get; set; } = null!;
|
||||
[Column("auth")] [StringLength(256)] public string AuthSecret { get; set; } = null!;
|
||||
|
||||
[Column("publickey")]
|
||||
[StringLength(128)]
|
||||
public string Publickey { get; set; } = null!;
|
||||
public string PublicKey { get; set; } = null!;
|
||||
|
||||
[Column("sendReadMessage")] public bool SendReadMessage { get; set; }
|
||||
|
||||
|
|
|
@ -440,6 +440,9 @@ public class User : IEntity
|
|||
[InverseProperty(nameof(SwSubscription.User))]
|
||||
public virtual ICollection<SwSubscription> SwSubscriptions { get; set; } = new List<SwSubscription>();
|
||||
|
||||
[InverseProperty(nameof(PushSubscription.User))]
|
||||
public virtual ICollection<PushSubscription> PushSubscriptions { get; set; } = new List<PushSubscription>();
|
||||
|
||||
[InverseProperty(nameof(UserGroupInvitation.User))]
|
||||
public virtual ICollection<UserGroupInvitation> UserGroupInvitations { get; set; } =
|
||||
new List<UserGroupInvitation>();
|
||||
|
|
|
@ -76,12 +76,14 @@ public static class ServiceExtensions
|
|||
.AddSingleton<RequestBufferingMiddleware>()
|
||||
.AddSingleton<AuthorizationMiddleware>()
|
||||
.AddSingleton<RequestVerificationMiddleware>()
|
||||
.AddSingleton<RequestDurationMiddleware>();
|
||||
.AddSingleton<RequestDurationMiddleware>()
|
||||
.AddSingleton<PushService>();
|
||||
|
||||
// Hosted services = long running background tasks
|
||||
// Note: These need to be added as a singleton as well to ensure data consistency
|
||||
services.AddHostedService<CronService>(provider => provider.GetRequiredService<CronService>());
|
||||
services.AddHostedService<QueueService>(provider => provider.GetRequiredService<QueueService>());
|
||||
services.AddHostedService<PushService>(provider => provider.GetRequiredService<PushService>());
|
||||
}
|
||||
|
||||
public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration)
|
||||
|
|
149
Iceshrimp.Backend/Core/Services/PushService.cs
Normal file
149
Iceshrimp.Backend/Core/Services/PushService.cs
Normal file
|
@ -0,0 +1,149 @@
|
|||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using WebPush;
|
||||
using PushSubscription = Iceshrimp.Backend.Core.Database.Tables.PushSubscription;
|
||||
using WebPushSubscription = WebPush.PushSubscription;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Services;
|
||||
|
||||
public class PushService(
|
||||
EventService eventSvc,
|
||||
ILogger<PushService> logger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
HttpClient httpClient,
|
||||
IOptions<Config.InstanceSection> config
|
||||
) : BackgroundService
|
||||
{
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
eventSvc.Notification += MastodonPushHandler;
|
||||
//TODO: eventSvc.Notification += WebPushHandler;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void MastodonPushHandler(object? _, Notification notification)
|
||||
{
|
||||
try
|
||||
{
|
||||
var provider = scopeFactory.CreateScope().ServiceProvider;
|
||||
var db = provider.GetRequiredService<DatabaseContext>();
|
||||
var subscriptions = await db.PushSubscriptions.Where(p => p.User == notification.Notifiee)
|
||||
.Include(pushSubscription => pushSubscription.OauthToken)
|
||||
.Where(p => p.Types.Contains(NotificationEntity.EncodeType(notification.Type)))
|
||||
.ToListAsync();
|
||||
|
||||
if (subscriptions.Count == 0)
|
||||
return;
|
||||
|
||||
var isSelf = notification.Notifier == notification.Notifiee;
|
||||
|
||||
var followed = subscriptions.All(p => p.Policy != PushSubscription.PushPolicy.Followed) ||
|
||||
isSelf ||
|
||||
await db.Followings.AnyAsync(p => p.Follower == notification.Notifiee &&
|
||||
p.Followee == notification.Notifier);
|
||||
|
||||
var follower = subscriptions.All(p => p.Policy != PushSubscription.PushPolicy.Follower) ||
|
||||
isSelf ||
|
||||
await db.Followings.AnyAsync(p => p.Follower == notification.Notifier &&
|
||||
p.Followee == notification.Notifiee);
|
||||
|
||||
try
|
||||
{
|
||||
var renderer = provider.GetRequiredService<NotificationRenderer>();
|
||||
var rendered = await renderer.RenderAsync(notification, notification.Notifiee);
|
||||
var name = rendered.Notifier.DisplayName;
|
||||
var subject = rendered.Type switch
|
||||
{
|
||||
"favourite" => $"{name} favorited your post",
|
||||
"follow" => $"{name} is now following you",
|
||||
"follow_request" => $"Pending follower: {name}",
|
||||
"mention" => $"You were mentioned by {name}",
|
||||
"poll" => $"A poll by {name} has ended",
|
||||
"reblog" => $"{name} boosted your post",
|
||||
"status" => $"{name} just posted",
|
||||
"update" => $"{name} edited a post",
|
||||
_ => $"New notification from {name}"
|
||||
};
|
||||
|
||||
var body = "";
|
||||
|
||||
if (notification.Note != null)
|
||||
body = notification.Note.Cw ?? notification.Note.Text ?? "";
|
||||
|
||||
body = body.Trim().Truncate(140).TrimEnd();
|
||||
if (body.Length > 137)
|
||||
body = body.Truncate(137).TrimEnd() + "...";
|
||||
|
||||
var client = new WebPushClient(httpClient);
|
||||
client.SetVapidDetails(new VapidDetails($"https://{config.Value.WebDomain}",
|
||||
Constants.VapidPublicKey, Constants.VapidPrivateKey));
|
||||
|
||||
var matchingSubscriptions =
|
||||
from subscription in subscriptions
|
||||
where subscription.Policy != PushSubscription.PushPolicy.Followed || followed
|
||||
where subscription.Policy != PushSubscription.PushPolicy.Follower || follower
|
||||
where subscription.Policy != PushSubscription.PushPolicy.None || isSelf
|
||||
select subscription;
|
||||
|
||||
foreach (var subscription in matchingSubscriptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = new PushSchemas.PushNotification
|
||||
{
|
||||
AccessToken = subscription.OauthToken.Token,
|
||||
NotificationType = rendered.Type,
|
||||
NotificationId = long.Parse(rendered.Id),
|
||||
IconUrl = rendered.Notifier.AvatarUrl,
|
||||
Title = subject,
|
||||
Body = body
|
||||
};
|
||||
|
||||
var sub = new WebPushSubscription
|
||||
{
|
||||
Endpoint = subscription.Endpoint,
|
||||
P256DH = subscription.PublicKey,
|
||||
Auth = subscription.AuthSecret,
|
||||
PushMode = PushMode.AesGcm
|
||||
};
|
||||
|
||||
await client.SendNotificationAsync(sub, JsonSerializer.Serialize(res));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case WebPushException { StatusCode: HttpStatusCode.Gone }:
|
||||
await db.PushSubscriptions.Where(p => p.Id == subscription.Id).ExecuteDeleteAsync();
|
||||
break;
|
||||
case WebPushException we:
|
||||
logger.LogDebug("Push notification delivery failed: {e}", we.Message);
|
||||
break;
|
||||
default:
|
||||
logger.LogDebug("Push notification delivery threw exception: {e}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (GracefulException)
|
||||
{
|
||||
// Unsupported notification type
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError("Event handler MastodonPushHandler threw exception: {e}", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,48 +16,49 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Amazon.S3" Version="0.31.2"/>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0"/>
|
||||
<PackageReference Include="Asp.Versioning.Http" Version="8.0.0"/>
|
||||
<PackageReference Include="AsyncKeyedLock" Version="6.3.4"/>
|
||||
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0"/>
|
||||
<PackageReference Include="cuid.net" Version="5.0.2"/>
|
||||
<PackageReference Include="Amazon.S3" Version="0.31.2" />
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Http" Version="8.0.0" />
|
||||
<PackageReference Include="AsyncKeyedLock" Version="6.3.4" />
|
||||
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" />
|
||||
<PackageReference Include="cuid.net" Version="5.0.2" />
|
||||
<PackageReference Include="dotNetRdf.Core" Version="3.2.5-iceshrimp" />
|
||||
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.0.0"/>
|
||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="3.0.4"/>
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0"/>
|
||||
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="3.0.4" />
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||
<PackageReference Include="libsodium" Version="1.0.18.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0"/>
|
||||
<PackageReference Include="protobuf-net" Version="3.2.30"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.1"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.17"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
|
||||
<PackageReference Include="System.IO.Hashing" Version="8.0.0"/>
|
||||
<PackageReference Include="Vite.AspNetCore" Version="1.11.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.30" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.1" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.17" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
|
||||
<PackageReference Include="Vite.AspNetCore" Version="1.11.0" />
|
||||
<PackageReference Include="WebPush" Version="1.0.24-iceshrimp" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Pages\Error.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\Shared\_Layout.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\_ViewImports.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\_ViewStart.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\Error.cshtml" />
|
||||
<AdditionalFiles Include="Pages\Shared\_Layout.cshtml" />
|
||||
<AdditionalFiles Include="Pages\_ViewImports.cshtml" />
|
||||
<AdditionalFiles Include="Pages\_ViewStart.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="wwwroot\.vite\manifest.json" CopyToPublishDirectory="PreserveNewest"/>
|
||||
<Content Include="wwwroot\.vite\manifest.json" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="migrate.sql"/>
|
||||
<None Remove="migrate.sql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
Loading…
Add table
Reference in a new issue