Compare commits
4 Commits
1a137fbb6a
...
cebd1bd65a
Author | SHA1 | Date | |
---|---|---|---|
cebd1bd65a | |||
da58e10d88 | |||
d492c9ce1f | |||
f170793928 |
@ -1,19 +1,43 @@
|
|||||||
|
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Discovery;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Activity;
|
namespace DysonNetwork.Sphere.Activity;
|
||||||
|
|
||||||
public class ActivityService(AppDatabase db, PublisherService pub, RelationshipService rels, PostService ps)
|
public class ActivityService(AppDatabase db, PublisherService pub, RelationshipService rels, PostService ps, DiscoveryService ds)
|
||||||
{
|
{
|
||||||
|
private double CalculateHotRank(Post.Post post, Instant now)
|
||||||
|
{
|
||||||
|
var score = post.Upvotes - post.Downvotes;
|
||||||
|
var postTime = post.PublishedAt ?? post.CreatedAt;
|
||||||
|
var hours = (now - postTime).TotalHours;
|
||||||
|
// Add 1 to score to prevent negative results for posts with more downvotes than upvotes
|
||||||
|
return (score + 1) / Math.Pow(hours + 2, 1.8);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<Activity>> GetActivitiesForAnyone(int take, Instant? cursor)
|
public async Task<List<Activity>> GetActivitiesForAnyone(int take, Instant? cursor)
|
||||||
{
|
{
|
||||||
var activities = new List<Activity>();
|
var activities = new List<Activity>();
|
||||||
|
|
||||||
// Crunching up data
|
if (cursor == null)
|
||||||
var posts = await db.Posts
|
{
|
||||||
|
var realms = await ds.GetPublicRealmsAsync(null, null);
|
||||||
|
if (realms.Count > 0)
|
||||||
|
{
|
||||||
|
activities.Add(new DiscoveryActivity("Explore Realms", realms.Cast<object>().ToList()).ToActivity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch a larger batch of recent posts to rank
|
||||||
|
var postsQuery = db.Posts
|
||||||
.Include(e => e.RepliedPost)
|
.Include(e => e.RepliedPost)
|
||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
@ -22,8 +46,9 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
.Where(p => cursor == null || p.PublishedAt < cursor)
|
.Where(p => cursor == null || p.PublishedAt < cursor)
|
||||||
.OrderByDescending(p => p.PublishedAt)
|
.OrderByDescending(p => p.PublishedAt)
|
||||||
.FilterWithVisibility(null, [], [], isListing: true)
|
.FilterWithVisibility(null, [], [], isListing: true)
|
||||||
.Take(take)
|
.Take(take * 5); // Fetch more posts to have a good pool for ranking
|
||||||
.ToListAsync();
|
|
||||||
|
var posts = await postsQuery.ToListAsync();
|
||||||
posts = await ps.LoadPostInfo(posts, null, true);
|
posts = await ps.LoadPostInfo(posts, null, true);
|
||||||
|
|
||||||
var postsId = posts.Select(e => e.Id).ToList();
|
var postsId = posts.Select(e => e.Id).ToList();
|
||||||
@ -32,8 +57,17 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
post.ReactionsCount =
|
post.ReactionsCount =
|
||||||
reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary<string, int>();
|
reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary<string, int>();
|
||||||
|
|
||||||
|
// Rank and sort
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var rankedPosts = posts
|
||||||
|
.Select(p => new { Post = p, Rank = CalculateHotRank(p, now) })
|
||||||
|
.OrderByDescending(x => x.Rank)
|
||||||
|
.Select(x => x.Post)
|
||||||
|
.Take(take)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// Formatting data
|
// Formatting data
|
||||||
foreach (var post in posts)
|
foreach (var post in rankedPosts)
|
||||||
activities.Add(post.ToActivity());
|
activities.Add(post.ToActivity());
|
||||||
|
|
||||||
if (activities.Count == 0)
|
if (activities.Count == 0)
|
||||||
@ -53,6 +87,15 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
var userFriends = await rels.ListAccountFriends(currentUser);
|
var userFriends = await rels.ListAccountFriends(currentUser);
|
||||||
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
||||||
|
|
||||||
|
if (cursor == null)
|
||||||
|
{
|
||||||
|
var realms = await ds.GetPublicRealmsAsync(null, null);
|
||||||
|
if (realms.Count > 0)
|
||||||
|
{
|
||||||
|
activities.Add(new DiscoveryActivity("Explore Realms", realms.Cast<object>().ToList()).ToActivity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get publishers based on filter
|
// Get publishers based on filter
|
||||||
var filteredPublishers = filter switch
|
var filteredPublishers = filter switch
|
||||||
{
|
{
|
||||||
@ -81,7 +124,7 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
// Complete the query with visibility filtering and execute
|
// Complete the query with visibility filtering and execute
|
||||||
var posts = await postsQuery
|
var posts = await postsQuery
|
||||||
.FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true)
|
.FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true)
|
||||||
.Take(take)
|
.Take(take * 5) // Fetch more posts to have a good pool for ranking
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
||||||
@ -97,8 +140,17 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString());
|
await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rank and sort
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var rankedPosts = posts
|
||||||
|
.Select(p => new { Post = p, Rank = CalculateHotRank(p, now) })
|
||||||
|
.OrderByDescending(x => x.Rank)
|
||||||
|
.Select(x => x.Post)
|
||||||
|
.Take(take)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// Formatting data
|
// Formatting data
|
||||||
foreach (var post in posts)
|
foreach (var post in rankedPosts)
|
||||||
activities.Add(post.ToActivity());
|
activities.Add(post.ToActivity());
|
||||||
|
|
||||||
if (activities.Count == 0)
|
if (activities.Count == 0)
|
||||||
@ -106,4 +158,4 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
|
|
||||||
return activities;
|
return activities;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
DysonNetwork.Sphere/Activity/DiscoveryActivity.cs
Normal file
32
DysonNetwork.Sphere/Activity/DiscoveryActivity.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Activity
|
||||||
|
{
|
||||||
|
public class DiscoveryActivity : IActivity
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public List<object> Items { get; set; }
|
||||||
|
|
||||||
|
public DiscoveryActivity(string title, List<object> items)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
Items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Activity ToActivity()
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
return new Activity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Type = "discovery",
|
||||||
|
ResourceIdentifier = "discovery",
|
||||||
|
Data = this,
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -75,6 +75,8 @@ public class AppDatabase(
|
|||||||
|
|
||||||
public DbSet<Realm.Realm> Realms { get; set; }
|
public DbSet<Realm.Realm> Realms { get; set; }
|
||||||
public DbSet<RealmMember> RealmMembers { get; set; }
|
public DbSet<RealmMember> RealmMembers { get; set; }
|
||||||
|
public DbSet<Tag> Tags { get; set; }
|
||||||
|
public DbSet<RealmTag> RealmTags { get; set; }
|
||||||
|
|
||||||
public DbSet<ChatRoom> ChatRooms { get; set; }
|
public DbSet<ChatRoom> ChatRooms { get; set; }
|
||||||
public DbSet<ChatMember> ChatMembers { get; set; }
|
public DbSet<ChatMember> ChatMembers { get; set; }
|
||||||
@ -230,6 +232,19 @@ public class AppDatabase(
|
|||||||
.HasForeignKey(pm => pm.AccountId)
|
.HasForeignKey(pm => pm.AccountId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
modelBuilder.Entity<RealmTag>()
|
||||||
|
.HasKey(rt => new { rt.RealmId, rt.TagId });
|
||||||
|
modelBuilder.Entity<RealmTag>()
|
||||||
|
.HasOne(rt => rt.Realm)
|
||||||
|
.WithMany(r => r.RealmTags)
|
||||||
|
.HasForeignKey(rt => rt.RealmId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<RealmTag>()
|
||||||
|
.HasOne(rt => rt.Tag)
|
||||||
|
.WithMany(t => t.RealmTags)
|
||||||
|
.HasForeignKey(rt => rt.TagId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
modelBuilder.Entity<ChatMember>()
|
modelBuilder.Entity<ChatMember>()
|
||||||
.HasKey(pm => new { pm.Id });
|
.HasKey(pm => new { pm.Id });
|
||||||
modelBuilder.Entity<ChatMember>()
|
modelBuilder.Entity<ChatMember>()
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||||
|
|
||||||
|
public class ScrapedArticle
|
||||||
|
{
|
||||||
|
public LinkEmbed LinkEmbed { get; set; } = null!;
|
||||||
|
public string? Content { get; set; }
|
||||||
|
}
|
@ -23,6 +23,11 @@ public class WebArticle : ModelBase
|
|||||||
public WebFeed Feed { get; set; } = null!;
|
public WebFeed Feed { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class WebFeedConfig
|
||||||
|
{
|
||||||
|
public bool ScrapPage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class WebFeed : ModelBase
|
public class WebFeed : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
@ -31,6 +36,7 @@ public class WebFeed : ModelBase
|
|||||||
[MaxLength(8192)] public string? Description { get; set; }
|
[MaxLength(8192)] public string? Description { get; set; }
|
||||||
|
|
||||||
[Column(TypeName = "jsonb")] public LinkEmbed? Preview { get; set; }
|
[Column(TypeName = "jsonb")] public LinkEmbed? Preview { get; set; }
|
||||||
|
[Column(TypeName = "jsonb")] public WebFeedConfig Config { get; set; } = new();
|
||||||
|
|
||||||
public Guid PublisherId { get; set; }
|
public Guid PublisherId { get; set; }
|
||||||
public Publisher.Publisher Publisher { get; set; } = null!;
|
public Publisher.Publisher Publisher { get; set; } = null!;
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Sphere.Permission;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Connection.WebReader;
|
namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("feeds")]
|
[Route("feeds")]
|
||||||
public class WebFeedController(WebFeedService webFeedService) : ControllerBase
|
public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase
|
||||||
{
|
{
|
||||||
public class CreateWebFeedRequest
|
public class CreateWebFeedRequest
|
||||||
{
|
{
|
||||||
@ -30,4 +32,31 @@ public class WebFeedController(WebFeedService webFeedService) : ControllerBase
|
|||||||
var feed = await webFeedService.CreateWebFeedAsync(request, User);
|
var feed = await webFeedService.CreateWebFeedAsync(request, User);
|
||||||
return Ok(feed);
|
return Ok(feed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("scrape/{feedId}")]
|
||||||
|
[RequiredPermission("maintenance", "web-feeds")]
|
||||||
|
public async Task<ActionResult> ScrapeFeed(Guid feedId)
|
||||||
|
{
|
||||||
|
var feed = await database.Set<WebFeed>().FindAsync(feedId);
|
||||||
|
if (feed == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
await webFeedService.ScrapeFeedAsync(feed);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("scrape-all")]
|
||||||
|
[RequiredPermission("maintenance", "web-feeds")]
|
||||||
|
public async Task<ActionResult> ScrapeAllFeeds()
|
||||||
|
{
|
||||||
|
var feeds = await database.Set<WebFeed>().ToListAsync();
|
||||||
|
foreach (var feed in feeds)
|
||||||
|
{
|
||||||
|
await webFeedService.ScrapeFeedAsync(feed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,12 @@ public class WebFeedService(
|
|||||||
AppDatabase database,
|
AppDatabase database,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
ILogger<WebFeedService> logger,
|
ILogger<WebFeedService> logger,
|
||||||
AccountService accountService
|
AccountService accountService,
|
||||||
|
WebReaderService webReaderService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<WebFeed> CreateWebFeedAsync(WebFeedController.CreateWebFeedRequest dto, ClaimsPrincipal claims)
|
public async Task<WebFeed> CreateWebFeedAsync(WebFeedController.CreateWebFeedRequest request,
|
||||||
|
ClaimsPrincipal claims)
|
||||||
{
|
{
|
||||||
if (claims.Identity?.Name == null)
|
if (claims.Identity?.Name == null)
|
||||||
{
|
{
|
||||||
@ -29,9 +31,9 @@ public class WebFeedService(
|
|||||||
|
|
||||||
var feed = new WebFeed
|
var feed = new WebFeed
|
||||||
{
|
{
|
||||||
Url = dto.Url,
|
Url = request.Url,
|
||||||
Title = dto.Title,
|
Title = request.Title,
|
||||||
Description = dto.Description,
|
Description = request.Description,
|
||||||
PublisherId = account.Id,
|
PublisherId = account.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -73,14 +75,29 @@ public class WebFeedService(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text;
|
||||||
|
LinkEmbed preview;
|
||||||
|
|
||||||
|
if (feed.Config.ScrapPage)
|
||||||
|
{
|
||||||
|
var scrapedArticle = await webReaderService.ScrapeArticleAsync(itemUrl, cancellationToken);
|
||||||
|
preview = scrapedArticle.LinkEmbed;
|
||||||
|
content = scrapedArticle.Content;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
preview = await webReaderService.GetLinkPreviewAsync(itemUrl, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
var newArticle = new WebArticle
|
var newArticle = new WebArticle
|
||||||
{
|
{
|
||||||
FeedId = feed.Id,
|
FeedId = feed.Id,
|
||||||
Title = item.Title.Text,
|
Title = item.Title.Text,
|
||||||
Url = itemUrl,
|
Url = itemUrl,
|
||||||
Author = item.Authors.FirstOrDefault()?.Name,
|
Author = item.Authors.FirstOrDefault()?.Name,
|
||||||
Content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text,
|
Content = content,
|
||||||
PublishedAt = item.PublishDate.UtcDateTime,
|
PublishedAt = item.PublishDate.UtcDateTime,
|
||||||
|
Preview = preview,
|
||||||
};
|
};
|
||||||
|
|
||||||
database.Set<WebArticle>().Add(newArticle);
|
database.Set<WebArticle>().Add(newArticle);
|
||||||
|
@ -2,6 +2,7 @@ using System.Globalization;
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Connection.WebReader;
|
namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||||
|
|
||||||
@ -17,6 +18,30 @@ public class WebReaderService(
|
|||||||
private const string LinkPreviewCachePrefix = "scrap:preview:";
|
private const string LinkPreviewCachePrefix = "scrap:preview:";
|
||||||
private const string LinkPreviewCacheGroup = "scrap:preview";
|
private const string LinkPreviewCacheGroup = "scrap:preview";
|
||||||
|
|
||||||
|
public async Task<ScrapedArticle> ScrapeArticleAsync(string url, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var linkEmbed = await GetLinkPreviewAsync(url, cancellationToken);
|
||||||
|
var content = await GetArticleContentAsync(url, cancellationToken);
|
||||||
|
return new ScrapedArticle
|
||||||
|
{
|
||||||
|
LinkEmbed = linkEmbed,
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetArticleContentAsync(string url, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var httpClient = httpClientFactory.CreateClient("WebReader");
|
||||||
|
var response = await httpClient.GetAsync(url, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var html = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(html);
|
||||||
|
var articleNode = doc.DocumentNode.SelectSingleNode("//article");
|
||||||
|
return articleNode?.InnerHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a link preview embed from a URL
|
/// Generate a link preview embed from a URL
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
17
DysonNetwork.Sphere/Discovery/DiscoveryController.cs
Normal file
17
DysonNetwork.Sphere/Discovery/DiscoveryController.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DysonNetwork.Sphere.Realm;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Discovery;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("discovery")]
|
||||||
|
public class DiscoveryController(DiscoveryService discoveryService) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("realms")]
|
||||||
|
public Task<List<Realm.Realm>> GetPublicRealms([FromQuery] string? query, [FromQuery] List<string>? tags)
|
||||||
|
{
|
||||||
|
return discoveryService.GetPublicRealmsAsync(query, tags);
|
||||||
|
}
|
||||||
|
}
|
29
DysonNetwork.Sphere/Discovery/DiscoveryService.cs
Normal file
29
DysonNetwork.Sphere/Discovery/DiscoveryService.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
using DysonNetwork.Sphere.Realm;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Discovery;
|
||||||
|
|
||||||
|
public class DiscoveryService(AppDatabase appDatabase)
|
||||||
|
{
|
||||||
|
public Task<List<Realm.Realm>> GetPublicRealmsAsync(string? query, List<string>? tags)
|
||||||
|
{
|
||||||
|
var realmsQuery = appDatabase.Realms
|
||||||
|
.Where(r => r.IsPublic);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(query))
|
||||||
|
{
|
||||||
|
realmsQuery = realmsQuery.Where(r => r.Name.Contains(query) || r.Description.Contains(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags != null && tags.Count > 0)
|
||||||
|
{
|
||||||
|
realmsQuery = realmsQuery.Where(r => r.RealmTags.Any(rt => tags.Contains(rt.Tag.Name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return realmsQuery.ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@
|
|||||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
|
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||||
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
|
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
|
||||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||||
@ -83,6 +84,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Migrations\" />
|
<Folder Include="Migrations\" />
|
||||||
|
<Folder Include="Discovery\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
3947
DysonNetwork.Sphere/Migrations/20250626105203_AddRealmTags.Designer.cs
generated
Normal file
3947
DysonNetwork.Sphere/Migrations/20250626105203_AddRealmTags.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using DysonNetwork.Sphere.Connection.WebReader;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRealmTags : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<WebFeedConfig>(
|
||||||
|
name: "config",
|
||||||
|
table: "web_feeds",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: false);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "tags",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
name = table.Column<string>(type: "character varying(64)", maxLength: 64, 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_tags", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "realm_tags",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
realm_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tag_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_realm_tags", x => new { x.realm_id, x.tag_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_realm_tags_realms_realm_id",
|
||||||
|
column: x => x.realm_id,
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_realm_tags_tags_tag_id",
|
||||||
|
column: x => x.tag_id,
|
||||||
|
principalTable: "tags",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_realm_tags_tag_id",
|
||||||
|
table: "realm_tags",
|
||||||
|
column: "tag_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "realm_tags");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "tags");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "config",
|
||||||
|
table: "web_feeds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1438,6 +1438,11 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<WebFeedConfig>("Config")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("config");
|
||||||
|
|
||||||
b.Property<Instant>("CreatedAt")
|
b.Property<Instant>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("created_at");
|
.HasColumnName("created_at");
|
||||||
@ -2359,6 +2364,68 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.ToTable("realm_members", (string)null);
|
b.ToTable("realm_members", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RealmId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("realm_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("TagId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tag_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<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("RealmId", "TagId")
|
||||||
|
.HasName("pk_realm_tags");
|
||||||
|
|
||||||
|
b.HasIndex("TagId")
|
||||||
|
.HasDatabaseName("ix_realm_tags_tag_id");
|
||||||
|
|
||||||
|
b.ToTable("realm_tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("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>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_tags");
|
||||||
|
|
||||||
|
b.ToTable("tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -3573,6 +3640,27 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.Navigation("Realm");
|
b.Navigation("Realm");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm")
|
||||||
|
.WithMany("RealmTags")
|
||||||
|
.HasForeignKey("RealmId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_realm_tags_realms_realm_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Sphere.Realm.Tag", "Tag")
|
||||||
|
.WithMany("RealmTags")
|
||||||
|
.HasForeignKey("TagId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_realm_tags_tags_tag_id");
|
||||||
|
|
||||||
|
b.Navigation("Realm");
|
||||||
|
|
||||||
|
b.Navigation("Tag");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack")
|
b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack")
|
||||||
@ -3837,6 +3925,13 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.Navigation("ChatRooms");
|
b.Navigation("ChatRooms");
|
||||||
|
|
||||||
b.Navigation("Members");
|
b.Navigation("Members");
|
||||||
|
|
||||||
|
b.Navigation("RealmTags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RealmTags");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b =>
|
||||||
|
@ -29,6 +29,7 @@ public class Realm : ModelBase, IIdentifiedResource
|
|||||||
|
|
||||||
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
|
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
|
||||||
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
|
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
|
||||||
|
[JsonIgnore] public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||||
|
12
DysonNetwork.Sphere/Realm/RealmTag.cs
Normal file
12
DysonNetwork.Sphere/Realm/RealmTag.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Realm;
|
||||||
|
|
||||||
|
public class RealmTag : ModelBase
|
||||||
|
{
|
||||||
|
public Guid RealmId { get; set; }
|
||||||
|
public Realm Realm { get; set; } = null!;
|
||||||
|
|
||||||
|
public Guid TagId { get; set; }
|
||||||
|
public Tag Tag { get; set; } = null!;
|
||||||
|
}
|
14
DysonNetwork.Sphere/Realm/Tag.cs
Normal file
14
DysonNetwork.Sphere/Realm/Tag.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Realm;
|
||||||
|
|
||||||
|
public class Tag : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
|
||||||
|
}
|
@ -25,6 +25,7 @@ using StackExchange.Redis;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
using DysonNetwork.Sphere.Connection.WebReader;
|
using DysonNetwork.Sphere.Connection.WebReader;
|
||||||
|
using DysonNetwork.Sphere.Discovery;
|
||||||
using DysonNetwork.Sphere.Safety;
|
using DysonNetwork.Sphere.Safety;
|
||||||
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
||||||
using tusdotnet.Stores;
|
using tusdotnet.Stores;
|
||||||
@ -227,6 +228,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<WebFeedService>();
|
services.AddScoped<WebFeedService>();
|
||||||
services.AddScoped<AfdianPaymentHandler>();
|
services.AddScoped<AfdianPaymentHandler>();
|
||||||
services.AddScoped<SafetyService>();
|
services.AddScoped<SafetyService>();
|
||||||
|
services.AddScoped<DiscoveryService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using FFMpegCore;
|
using FFMpegCore;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using AngleSharp.Text;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Minio;
|
using Minio;
|
||||||
using Minio.DataModel.Args;
|
using Minio.DataModel.Args;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
|
||||||
using tusdotnet.Stores;
|
using tusdotnet.Stores;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Sphere.Storage;
|
||||||
@ -54,7 +53,7 @@ public class FileService(
|
|||||||
private static readonly string TempFilePrefix = "dyn-cloudfile";
|
private static readonly string TempFilePrefix = "dyn-cloudfile";
|
||||||
|
|
||||||
private static readonly string[] AnimatedImageTypes =
|
private static readonly string[] AnimatedImageTypes =
|
||||||
new[] { "image/gif", "image/apng", "image/webp", "image/avif" };
|
["image/gif", "image/apng", "image/webp", "image/avif"];
|
||||||
|
|
||||||
// The analysis file method no longer will remove the GPS EXIF data
|
// The analysis file method no longer will remove the GPS EXIF data
|
||||||
// It should be handled on the client side, and for some specific cases it should be keep
|
// It should be handled on the client side, and for some specific cases it should be keep
|
||||||
@ -115,6 +114,14 @@ public class FileService(
|
|||||||
|
|
||||||
// Try to get orientation from exif data
|
// Try to get orientation from exif data
|
||||||
var orientation = 1;
|
var orientation = 1;
|
||||||
|
var meta = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["blur"] = blurhash,
|
||||||
|
["format"] = format,
|
||||||
|
["width"] = width,
|
||||||
|
["height"] = height,
|
||||||
|
["orientation"] = orientation,
|
||||||
|
};
|
||||||
Dictionary<string, object> exif = [];
|
Dictionary<string, object> exif = [];
|
||||||
|
|
||||||
foreach (var field in vipsImage.GetFields())
|
foreach (var field in vipsImage.GetFields())
|
||||||
@ -122,10 +129,12 @@ public class FileService(
|
|||||||
var value = vipsImage.Get(field);
|
var value = vipsImage.Get(field);
|
||||||
|
|
||||||
// Skip GPS-related EXIF fields to remove location data
|
// Skip GPS-related EXIF fields to remove location data
|
||||||
if (IsGpsExifField(field))
|
if (IsIgnoredField(field))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
exif.Add(field, value);
|
if (field.StartsWith("exif-")) exif.Add(field.Replace("exif-", ""), value);
|
||||||
|
else meta.Add(field, value);
|
||||||
|
|
||||||
if (field == "orientation") orientation = (int)value;
|
if (field == "orientation") orientation = (int)value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,16 +143,9 @@ public class FileService(
|
|||||||
|
|
||||||
var aspectRatio = height != 0 ? (double)width / height : 0;
|
var aspectRatio = height != 0 ? (double)width / height : 0;
|
||||||
|
|
||||||
file.FileMeta = new Dictionary<string, object>
|
meta["exif"] = exif;
|
||||||
{
|
meta["ratio"] = aspectRatio;
|
||||||
["blur"] = blurhash,
|
file.FileMeta = meta;
|
||||||
["format"] = format,
|
|
||||||
["width"] = width,
|
|
||||||
["height"] = height,
|
|
||||||
["orientation"] = orientation,
|
|
||||||
["ratio"] = aspectRatio,
|
|
||||||
["exif"] = exif
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -188,7 +190,7 @@ public class FileService(
|
|||||||
{
|
{
|
||||||
// Skip compression for animated image types
|
// Skip compression for animated image types
|
||||||
var animatedMimeTypes = AnimatedImageTypes;
|
var animatedMimeTypes = AnimatedImageTypes;
|
||||||
if (animatedMimeTypes.Contains(contentType))
|
if (Enumerable.Contains(animatedMimeTypes, contentType))
|
||||||
{
|
{
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"File {fileId} is an animated image (MIME: {mime}), skipping WebP conversion.", fileId,
|
"File {fileId} is an animated image (MIME: {mime}), skipping WebP conversion.", fileId,
|
||||||
@ -541,4 +543,11 @@ public class FileService(
|
|||||||
fieldName.Equals(gpsField, StringComparison.OrdinalIgnoreCase) ||
|
fieldName.Equals(gpsField, StringComparison.OrdinalIgnoreCase) ||
|
||||||
fieldName.StartsWith("gps", StringComparison.OrdinalIgnoreCase));
|
fieldName.StartsWith("gps", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsIgnoredField(string fieldName)
|
||||||
|
{
|
||||||
|
if (IsGpsExifField(fieldName)) return true;
|
||||||
|
if (fieldName.EndsWith("-data")) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
@ -78,6 +78,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3bef61b8a21d4c8e96872ecdd7782fa0e55000_003F7a_003F870020d0_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3bef61b8a21d4c8e96872ecdd7782fa0e55000_003F7a_003F870020d0_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASyndicationFeed_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b43b9cf654743f8b9a2eee23c625dd21dd30_003Fad_003Fd26b4d73_003FSyndicationFeed_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATotp_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F48c9d2a1b3c84b32b36ebc6f20a927ea4600_003F7b_003Ff98e5727_003FTotp_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATotp_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F48c9d2a1b3c84b32b36ebc6f20a927ea4600_003F7b_003Ff98e5727_003FTotp_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user