Compare commits
4 Commits
1a137fbb6a
...
cebd1bd65a
Author | SHA1 | Date | |
---|---|---|---|
cebd1bd65a | |||
da58e10d88 | |||
d492c9ce1f | |||
f170793928 |
@ -1,19 +1,43 @@
|
||||
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Discovery;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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)
|
||||
{
|
||||
var activities = new List<Activity>();
|
||||
|
||||
// Crunching up data
|
||||
var posts = await db.Posts
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch a larger batch of recent posts to rank
|
||||
var postsQuery = db.Posts
|
||||
.Include(e => e.RepliedPost)
|
||||
.Include(e => e.ForwardedPost)
|
||||
.Include(e => e.Categories)
|
||||
@ -22,8 +46,9 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
||||
.Where(p => cursor == null || p.PublishedAt < cursor)
|
||||
.OrderByDescending(p => p.PublishedAt)
|
||||
.FilterWithVisibility(null, [], [], isListing: true)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
.Take(take * 5); // Fetch more posts to have a good pool for ranking
|
||||
|
||||
var posts = await postsQuery.ToListAsync();
|
||||
posts = await ps.LoadPostInfo(posts, null, true);
|
||||
|
||||
var postsId = posts.Select(e => e.Id).ToList();
|
||||
@ -32,8 +57,17 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
||||
post.ReactionsCount =
|
||||
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
|
||||
foreach (var post in posts)
|
||||
foreach (var post in rankedPosts)
|
||||
activities.Add(post.ToActivity());
|
||||
|
||||
if (activities.Count == 0)
|
||||
@ -53,6 +87,15 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
||||
var userFriends = await rels.ListAccountFriends(currentUser);
|
||||
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
|
||||
var filteredPublishers = filter switch
|
||||
{
|
||||
@ -81,7 +124,7 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
||||
// Complete the query with visibility filtering and execute
|
||||
var posts = await postsQuery
|
||||
.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();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// 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
|
||||
foreach (var post in posts)
|
||||
foreach (var post in rankedPosts)
|
||||
activities.Add(post.ToActivity());
|
||||
|
||||
if (activities.Count == 0)
|
||||
@ -106,4 +158,4 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
||||
|
||||
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<RealmMember> RealmMembers { get; set; }
|
||||
public DbSet<Tag> Tags { get; set; }
|
||||
public DbSet<RealmTag> RealmTags { get; set; }
|
||||
|
||||
public DbSet<ChatRoom> ChatRooms { get; set; }
|
||||
public DbSet<ChatMember> ChatMembers { get; set; }
|
||||
@ -230,6 +232,19 @@ public class AppDatabase(
|
||||
.HasForeignKey(pm => pm.AccountId)
|
||||
.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>()
|
||||
.HasKey(pm => new { pm.Id });
|
||||
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 class WebFeedConfig
|
||||
{
|
||||
public bool ScrapPage { get; set; }
|
||||
}
|
||||
|
||||
public class WebFeed : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
@ -31,6 +36,7 @@ public class WebFeed : ModelBase
|
||||
[MaxLength(8192)] public string? Description { 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 Publisher.Publisher Publisher { get; set; } = null!;
|
||||
|
@ -1,13 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("feeds")]
|
||||
public class WebFeedController(WebFeedService webFeedService) : ControllerBase
|
||||
public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase
|
||||
{
|
||||
public class CreateWebFeedRequest
|
||||
{
|
||||
@ -30,4 +32,31 @@ public class WebFeedController(WebFeedService webFeedService) : ControllerBase
|
||||
var feed = await webFeedService.CreateWebFeedAsync(request, User);
|
||||
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,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
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)
|
||||
{
|
||||
@ -29,9 +31,9 @@ public class WebFeedService(
|
||||
|
||||
var feed = new WebFeed
|
||||
{
|
||||
Url = dto.Url,
|
||||
Title = dto.Title,
|
||||
Description = dto.Description,
|
||||
Url = request.Url,
|
||||
Title = request.Title,
|
||||
Description = request.Description,
|
||||
PublisherId = account.Id,
|
||||
};
|
||||
|
||||
@ -73,14 +75,29 @@ public class WebFeedService(
|
||||
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
|
||||
{
|
||||
FeedId = feed.Id,
|
||||
Title = item.Title.Text,
|
||||
Url = itemUrl,
|
||||
Author = item.Authors.FirstOrDefault()?.Name,
|
||||
Content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text,
|
||||
Content = content,
|
||||
PublishedAt = item.PublishDate.UtcDateTime,
|
||||
Preview = preview,
|
||||
};
|
||||
|
||||
database.Set<WebArticle>().Add(newArticle);
|
||||
|
@ -2,6 +2,7 @@ using System.Globalization;
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||
|
||||
@ -17,6 +18,30 @@ public class WebReaderService(
|
||||
private const string LinkPreviewCachePrefix = "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>
|
||||
/// Generate a link preview embed from a URL
|
||||
/// </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.NamingConventions" Version="9.0.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="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||
@ -83,6 +84,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
<Folder Include="Discovery\" />
|
||||
</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")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<WebFeedConfig>("Config")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
@ -2359,6 +2364,68 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -3573,6 +3640,27 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack")
|
||||
@ -3837,6 +3925,13 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.Navigation("ChatRooms");
|
||||
|
||||
b.Navigation("Members");
|
||||
|
||||
b.Navigation("RealmTags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Tag", b =>
|
||||
{
|
||||
b.Navigation("RealmTags");
|
||||
});
|
||||
|
||||
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<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
|
||||
[JsonIgnore] public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[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.Threading.RateLimiting;
|
||||
using DysonNetwork.Sphere.Connection.WebReader;
|
||||
using DysonNetwork.Sphere.Discovery;
|
||||
using DysonNetwork.Sphere.Safety;
|
||||
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
||||
using tusdotnet.Stores;
|
||||
@ -227,6 +228,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<WebFeedService>();
|
||||
services.AddScoped<AfdianPaymentHandler>();
|
||||
services.AddScoped<SafetyService>();
|
||||
services.AddScoped<DiscoveryService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
using System.Globalization;
|
||||
using FFMpegCore;
|
||||
using System.Security.Cryptography;
|
||||
using AngleSharp.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Minio;
|
||||
using Minio.DataModel.Args;
|
||||
using NetVips;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using tusdotnet.Stores;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
@ -54,7 +53,7 @@ public class FileService(
|
||||
private static readonly string TempFilePrefix = "dyn-cloudfile";
|
||||
|
||||
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
|
||||
// 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
|
||||
var orientation = 1;
|
||||
var meta = new Dictionary<string, object>
|
||||
{
|
||||
["blur"] = blurhash,
|
||||
["format"] = format,
|
||||
["width"] = width,
|
||||
["height"] = height,
|
||||
["orientation"] = orientation,
|
||||
};
|
||||
Dictionary<string, object> exif = [];
|
||||
|
||||
foreach (var field in vipsImage.GetFields())
|
||||
@ -122,10 +129,12 @@ public class FileService(
|
||||
var value = vipsImage.Get(field);
|
||||
|
||||
// Skip GPS-related EXIF fields to remove location data
|
||||
if (IsGpsExifField(field))
|
||||
if (IsIgnoredField(field))
|
||||
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;
|
||||
}
|
||||
|
||||
@ -134,16 +143,9 @@ public class FileService(
|
||||
|
||||
var aspectRatio = height != 0 ? (double)width / height : 0;
|
||||
|
||||
file.FileMeta = new Dictionary<string, object>
|
||||
{
|
||||
["blur"] = blurhash,
|
||||
["format"] = format,
|
||||
["width"] = width,
|
||||
["height"] = height,
|
||||
["orientation"] = orientation,
|
||||
["ratio"] = aspectRatio,
|
||||
["exif"] = exif
|
||||
};
|
||||
meta["exif"] = exif;
|
||||
meta["ratio"] = aspectRatio;
|
||||
file.FileMeta = meta;
|
||||
}
|
||||
|
||||
break;
|
||||
@ -188,7 +190,7 @@ public class FileService(
|
||||
{
|
||||
// Skip compression for animated image types
|
||||
var animatedMimeTypes = AnimatedImageTypes;
|
||||
if (animatedMimeTypes.Contains(contentType))
|
||||
if (Enumerable.Contains(animatedMimeTypes, contentType))
|
||||
{
|
||||
logger.LogInformation(
|
||||
"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.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_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_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_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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user