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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user