Notification service

This commit is contained in:
LittleSheep 2025-04-27 23:44:03 +08:00
parent 3080e273cb
commit cb7179aa27
14 changed files with 2029 additions and 9 deletions

View 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!;
}

View 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}");
}
}
}

View File

@ -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; }

View File

@ -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()

View File

@ -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" />

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

View File

@ -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")

View File

@ -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();

View File

@ -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>();

View File

@ -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

View File

@ -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...");

View File

@ -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": ""
}
}
}
}

View File

@ -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>