Compare commits
3 Commits
205ccd66b3
...
18fde9f16c
| Author | SHA1 | Date | |
|---|---|---|---|
| 18fde9f16c | |||
| 4e794ceb9b | |||
| b40282e43a |
@@ -1,11 +1,12 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory)
|
||||
public class ChatService(AppDatabase db, FileService fs, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
public async Task<Message> SendMessageAsync(Message message, ChatMember sender, ChatRoom room)
|
||||
{
|
||||
@@ -16,6 +17,9 @@ public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory)
|
||||
db.ChatMessages.Add(message);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var files = message.Attachments.Distinct().ToList();
|
||||
if (files.Count != 0) await fs.MarkUsageRangeAsync(files, 1);
|
||||
|
||||
// Then start the delivery process
|
||||
// Using ConfigureAwait(false) is correct here since we don't need context to flow
|
||||
_ = Task.Run(() => DeliverMessageAsync(message, sender, room))
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using tencentyun;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
public class RealtimeChatConfiguration
|
||||
{
|
||||
public string Provider { get; set; } = null!;
|
||||
public int AppId { get; set; }
|
||||
[JsonIgnore] public string SecretKey { get; set; } = null!;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
@@ -20,46 +16,6 @@ public class RealtimeCallController(IConfiguration configuration, AppDatabase db
|
||||
private readonly RealtimeChatConfiguration _config =
|
||||
configuration.GetSection("RealtimeChat").Get<RealtimeChatConfiguration>()!;
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<RealtimeChatConfiguration> GetConfiguration()
|
||||
{
|
||||
return _config;
|
||||
}
|
||||
|
||||
public class RealtimeChatToken
|
||||
{
|
||||
public RealtimeChatConfiguration Config { get; set; } = null!;
|
||||
public string Token { get; set; } = null!;
|
||||
}
|
||||
|
||||
[HttpGet("{roomId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<RealtimeChatToken>> GetToken(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
return StatusCode(403,
|
||||
"You need to be a normal member to get the token for joining the realtime chatroom."
|
||||
);
|
||||
|
||||
var ongoingCall = await cs.GetCallOngoingAsync(roomId);
|
||||
if (ongoingCall is null) return BadRequest("No ongoing call.");
|
||||
|
||||
var api = new TLSSigAPIv2(_config.AppId, _config.SecretKey);
|
||||
var sig = api.GenSig(currentUser.Name);
|
||||
if (sig is null) return StatusCode(500, "Failed to generate the token.");
|
||||
|
||||
return Ok(new RealtimeChatToken
|
||||
{
|
||||
Config = _config,
|
||||
Token = sig
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{roomId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> StartCall(Guid roomId)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||
<PackageReference Include="CorePush" Version="4.3.0" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
|
||||
@@ -58,12 +58,8 @@
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0" />
|
||||
<PackageReference Include="tls-sig-api-v2" Version="1.0.1" />
|
||||
<PackageReference Include="tusdotnet" Version="2.8.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
3386
DysonNetwork.Sphere/Migrations/20250518041519_OptimizeDataStructure.Designer.cs
generated
Normal file
3386
DysonNetwork.Sphere/Migrations/20250518041519_OptimizeDataStructure.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class OptimizeDataStructure : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "chat_statuses");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "chat_read_receipts",
|
||||
columns: table => new
|
||||
{
|
||||
message_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
sender_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_chat_read_receipts", x => new { x.message_id, x.sender_id });
|
||||
table.ForeignKey(
|
||||
name: "fk_chat_read_receipts_chat_members_sender_id",
|
||||
column: x => x.sender_id,
|
||||
principalTable: "chat_members",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_chat_read_receipts_chat_messages_message_id",
|
||||
column: x => x.message_id,
|
||||
principalTable: "chat_messages",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_chat_read_receipts_message_id_sender_id",
|
||||
table: "chat_read_receipts",
|
||||
columns: new[] { "message_id", "sender_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_chat_read_receipts_sender_id",
|
||||
table: "chat_read_receipts",
|
||||
column: "sender_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "chat_read_receipts");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "chat_statuses",
|
||||
columns: table => new
|
||||
{
|
||||
message_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
sender_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_chat_statuses", x => new { x.message_id, x.sender_id });
|
||||
table.ForeignKey(
|
||||
name: "fk_chat_statuses_chat_members_sender_id",
|
||||
column: x => x.sender_id,
|
||||
principalTable: "chat_members",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_chat_statuses_chat_messages_message_id",
|
||||
column: x => x.message_id,
|
||||
principalTable: "chat_messages",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_chat_statuses_message_id_sender_id",
|
||||
table: "chat_statuses",
|
||||
columns: new[] { "message_id", "sender_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_chat_statuses_sender_id",
|
||||
table: "chat_statuses",
|
||||
column: "sender_id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1143,7 +1143,7 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.ToTable("chat_reactions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageStatus", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageReadReceipt", b =>
|
||||
{
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid")
|
||||
@@ -1161,25 +1161,21 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("read_at");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("MessageId", "SenderId")
|
||||
.HasName("pk_chat_statuses");
|
||||
.HasName("pk_chat_read_receipts");
|
||||
|
||||
b.HasIndex("SenderId")
|
||||
.HasDatabaseName("ix_chat_statuses_sender_id");
|
||||
.HasDatabaseName("ix_chat_read_receipts_sender_id");
|
||||
|
||||
b.HasIndex("MessageId", "SenderId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_chat_statuses_message_id_sender_id");
|
||||
.HasDatabaseName("ix_chat_read_receipts_message_id_sender_id");
|
||||
|
||||
b.ToTable("chat_statuses", (string)null);
|
||||
b.ToTable("chat_read_receipts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Chat.RealtimeCall", b =>
|
||||
@@ -2850,21 +2846,21 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.Navigation("Sender");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageStatus", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageReadReceipt", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Chat.Message", "Message")
|
||||
.WithMany("Statuses")
|
||||
.HasForeignKey("MessageId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_chat_statuses_chat_messages_message_id");
|
||||
.HasConstraintName("fk_chat_read_receipts_chat_messages_message_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender")
|
||||
.WithMany()
|
||||
.HasForeignKey("SenderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_chat_statuses_chat_members_sender_id");
|
||||
.HasConstraintName("fk_chat_read_receipts_chat_members_sender_id");
|
||||
|
||||
b.Navigation("Message");
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
using System.Globalization;
|
||||
using FFMpegCore;
|
||||
using System.Security.Cryptography;
|
||||
using Blurhash.ImageSharp;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio;
|
||||
using Minio.DataModel.Args;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
||||
using tusdotnet.Stores;
|
||||
using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
|
||||
@@ -35,7 +31,8 @@ public class FileService(
|
||||
)
|
||||
{
|
||||
var result = new List<(string filePath, string suffix)>();
|
||||
|
||||
|
||||
var ogFilePath = Path.Join(configuration.GetValue<string>("Tus:StorePath"), fileId);
|
||||
var fileSize = stream.Length;
|
||||
var hash = await HashFileAsync(stream, fileSize: fileSize);
|
||||
contentType ??= !fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName);
|
||||
@@ -53,32 +50,28 @@ public class FileService(
|
||||
switch (contentType.Split('/')[0])
|
||||
{
|
||||
case "image":
|
||||
var blurhash = BlurHashSharp.SkiaSharp.BlurHashEncoder.Encode(xComponent: 3, yComponent: 3, filename: ogFilePath);
|
||||
|
||||
// Rewind stream
|
||||
stream.Position = 0;
|
||||
// We still need ImageSharp for blurhash calculation
|
||||
using (var imageSharp = await Image.LoadAsync<Rgba32>(stream))
|
||||
|
||||
// Use NetVips for the rest
|
||||
using (var vipsImage = NetVips.Image.NewFromStream(stream))
|
||||
{
|
||||
var blurhash = Blurhasher.Encode(imageSharp, 3, 3);
|
||||
|
||||
// Reset stream position after ImageSharp read
|
||||
stream.Position = 0;
|
||||
|
||||
// Use NetVips for the rest
|
||||
using var vipsImage = NetVips.Image.NewFromStream(stream);
|
||||
|
||||
var width = vipsImage.Width;
|
||||
var height = vipsImage.Height;
|
||||
var format = vipsImage.Get("vips-loader") ?? "unknown";
|
||||
|
||||
// Try to get orientation from exif data
|
||||
ushort orientation = 1;
|
||||
List<IExifValue> exif = [];
|
||||
int orientation = 1;
|
||||
Dictionary<string, object> exif = [];
|
||||
|
||||
// NetVips supports reading exif with vipsImage.GetField("exif-ifd0-Orientation")
|
||||
// but we'll keep the ImageSharp exif handling for now
|
||||
var exifProfile = imageSharp.Metadata.ExifProfile;
|
||||
if (exifProfile?.Values.FirstOrDefault(e => e.Tag == ExifTag.Orientation)
|
||||
?.GetValue() is ushort o)
|
||||
orientation = o;
|
||||
foreach (var field in vipsImage.GetFields())
|
||||
{
|
||||
var value = vipsImage.Get(field);
|
||||
exif.Add(field, value);
|
||||
if (field == "orientation") orientation = (int)value;
|
||||
}
|
||||
|
||||
if (orientation is 6 or 8)
|
||||
(width, height) = (height, width);
|
||||
@@ -137,10 +130,7 @@ public class FileService(
|
||||
if (contentType.Split('/')[0] == "image")
|
||||
{
|
||||
file.MimeType = "image/webp";
|
||||
|
||||
List<Task> tasks = [];
|
||||
|
||||
var ogFilePath = Path.Join(configuration.GetValue<string>("Tus:StorePath"), file.Id);
|
||||
|
||||
using var vipsImage = NetVips.Image.NewFromFile(ogFilePath);
|
||||
var imagePath = Path.Join(Path.GetTempPath(), $"{TempFilePrefix}#{file.Id}");
|
||||
vipsImage.WriteToFile(imagePath + ".webp");
|
||||
@@ -159,8 +149,6 @@ public class FileService(
|
||||
result.Add((imageCompressedPath + ".webp", ".compressed"));
|
||||
file.HasCompression = true;
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -10,10 +10,10 @@ public class MessageReadReceiptFlushHandler(IServiceProvider serviceProvider) :
|
||||
{
|
||||
public async Task FlushAsync(IReadOnlyList<MessageReadReceipt> items)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||
var distinctItems = items.DistinctBy(x => new { x.MessageId, x.SenderId }).ToList();
|
||||
|
||||
await db.BulkInsertAsync(items);
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); await db.BulkInsertAsync(distinctItems);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
"FromName": "Alphabot",
|
||||
"SubjectPrefix": "Solar Network"
|
||||
},
|
||||
"RealtimeChat": {
|
||||
"Provider": "cloudflare"
|
||||
},
|
||||
"GeoIp": {
|
||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<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_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003Ff0_003F595b6eda_003FAuthenticationSchemeOptions_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_003ABlurHashEncoder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fb87f853683828cb934127af9a42b22cf516412af1e61ae2ff4935ae82aff_003FBlurHashEncoder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABodyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc5c8aba04a29d49c65d772c9ffcd93ac7eb38ccbb49a5f506518a0b9bdcaa75_003FBodyBuilder_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>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AChapterData_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fe6_003F64a6c0f7_003FChapterData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
||||
Reference in New Issue
Block a user