Iceshrimp.NET/Iceshrimp.Backend/Core/Services/PushService.cs
2024-11-20 00:48:29 +01:00

168 lines
No EOL
6.1 KiB
C#

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 Iceshrimp.WebPush;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using PushSubscription = Iceshrimp.Backend.Core.Database.Tables.PushSubscription;
using WebPushSubscription = Iceshrimp.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 += MastodonPushHandlerAsync;
//TODO: eventSvc.Notification += WebPushHandler;
return Task.CompletedTask;
}
private async void MastodonPushHandlerAsync(object? _, Notification notification)
{
try
{
await using var scope = scopeFactory.CreateAsyncScope();
await using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
var type = NotificationEntity.EncodeType(notification.Type, isPleroma: false);
var subscriptions = await db.PushSubscriptions.Where(p => p.User == notification.Notifiee)
.Include(pushSubscription => pushSubscription.OauthToken)
.Where(p => p.Types.Contains(type))
.ToListAsync();
if (subscriptions.Count == 0)
return;
var skip = await db.Blockings.AnyAsync(p => p.Blocker == notification.Notifiee &&
p.Blockee == notification.Notifier) ||
await db.Mutings.AnyAsync(p => p.Muter == notification.Notifiee &&
p.Mutee == notification.Notifier);
if (skip)
{
// Notifier is blocked or muted, so we shouldn't deliver the notification
return;
}
logger.LogDebug("Delivering mastodon push notification {id} for user {userId}", notification.Id,
notification.Notifiee.Id);
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);
var renderer = scope.ServiceProvider.GetRequiredService<NotificationRenderer>();
var rendered = await renderer.RenderAsync(notification, notification.Notifiee, isPleroma: false);
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",
"reaction" when rendered.Emoji is null => $"{name} reacted to your post",
"reaction" when rendered.Emoji is not null => $"{name} reacted with {rendered.Emoji} to your 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 meta = scope.ServiceProvider.GetRequiredService<MetaService>();
var (priv, pub) = await meta.GetManyAsync(MetaEntity.VapidPrivateKey, MetaEntity.VapidPublicKey);
var client = new WebPushClient(httpClient);
client.SetVapidDetails(new VapidDetails($"https://{config.Value.WebDomain}", pub, priv));
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);
}
}
}