119 lines
4.4 KiB
C#
119 lines
4.4 KiB
C#
using Microsoft.Extensions.Localization;
|
|||
|
|
|
||
|
|
using MongoDB.Entities;
|
||
|
|
|
||
|
|
using PrivaPub.ClientModels;
|
||
|
|
using PrivaPub.ClientModels.Post;
|
||
|
|
using PrivaPub.ClientModels.Social;
|
||
|
|
using PrivaPub.Models.Post;
|
||
|
|
using PrivaPub.Models.Social;
|
||
|
|
using PrivaPub.Resources;
|
||
|
|
using PrivaPub.StaticServices;
|
||
|
|
|
||
|
|
namespace PrivaPub.Domain.Timelines
|
||
|
|
{
|
||
|
|
public interface ITimelineService
|
||
|
|
{
|
||
|
|
Task<WebResult> Home(string rootUserId, string avatarId, string maxId, int limit, CancellationToken token);
|
||
|
|
Task<WebResult> Notifications(string rootUserId, string avatarId, string maxId, int limit, CancellationToken token);
|
||
|
|
Task<WebResult> MarkNotificationsRead(string rootUserId, string avatarId, CancellationToken token);
|
||
|
|
}
|
||
|
|
|
||
|
|
public class TimelineService : ITimelineService
|
||
|
|
{
|
||
|
|
const int MaxLimit = 40;
|
||
|
|
|
||
|
|
readonly DbEntities _dbEntities;
|
||
|
|
readonly IStringLocalizer<GenericRes> _localizer;
|
||
|
|
|
||
|
|
public TimelineService(DbEntities dbEntities, IStringLocalizer<GenericRes> localizer)
|
||
|
|
{
|
||
|
|
_dbEntities = dbEntities;
|
||
|
|
_localizer = localizer;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<WebResult> Home(string rootUserId, string avatarId, string maxId, int limit, CancellationToken token)
|
||
|
|
{
|
||
|
|
var result = new WebResult();
|
||
|
|
if (!await Owns(rootUserId, avatarId, token))
|
||
|
|
return result.Invalidate(_localizer["Avatar not found."], StatusCodes.Status404NotFound);
|
||
|
|
|
||
|
|
var query = _dbEntities.TimelineEntries.Match(e => e.AvatarId == avatarId);
|
||
|
|
if (!string.IsNullOrEmpty(maxId))
|
||
|
|
query.Match(f => f.Lt(e => e.PostId, maxId));
|
||
|
|
var entries = await query.Sort(e => e.PostId, Order.Descending).Limit(Math.Clamp(limit, 1, MaxLimit)).ExecuteAsync(token);
|
||
|
|
|
||
|
|
var ids = entries.Select(e => e.PostId).Concat(entries.Where(e => e.ReblogOfPostId != default).Select(e => e.ReblogOfPostId)).Distinct().ToList();
|
||
|
|
var posts = ids.Count == 0
|
||
|
|
? new Dictionary<string, Post>()
|
||
|
|
: (await _dbEntities.Posts.Match(p => ids.Contains(p.ID) && !p.DeletedAt.HasValue).ExecuteAsync(token)).ToDictionary(p => p.ID);
|
||
|
|
|
||
|
|
result.Data = entries
|
||
|
|
.Where(e => posts.ContainsKey(e.PostId) && (e.ReblogOfPostId == default || posts.ContainsKey(e.ReblogOfPostId)))
|
||
|
|
.Select(e =>
|
||
|
|
{
|
||
|
|
var view = ToView(posts[e.PostId]);
|
||
|
|
if (e.ReblogOfPostId != default)
|
||
|
|
view.ReblogOf = ToView(posts[e.ReblogOfPostId]);
|
||
|
|
return view;
|
||
|
|
})
|
||
|
|
.ToList();
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<WebResult> Notifications(string rootUserId, string avatarId, string maxId, int limit, CancellationToken token)
|
||
|
|
{
|
||
|
|
var result = new WebResult();
|
||
|
|
if (!await Owns(rootUserId, avatarId, token))
|
||
|
|
return result.Invalidate(_localizer["Avatar not found."], StatusCodes.Status404NotFound);
|
||
|
|
|
||
|
|
var query = _dbEntities.Notifications.Match(n => n.AvatarId == avatarId);
|
||
|
|
if (!string.IsNullOrEmpty(maxId))
|
||
|
|
query.Match(f => f.Lt(n => n.ID, maxId));
|
||
|
|
result.Data = (await query.Sort(n => n.ID, Order.Descending).Limit(Math.Clamp(limit, 1, MaxLimit)).ExecuteAsync(token))
|
||
|
|
.Select(n => new ViewNotification
|
||
|
|
{
|
||
|
|
Id = n.ID,
|
||
|
|
Type = n.Type.ToString(),
|
||
|
|
FromAccountId = n.FromAccountId,
|
||
|
|
FromActorURI = n.FromActorURI,
|
||
|
|
PostId = n.PostId,
|
||
|
|
IsRead = n.IsRead,
|
||
|
|
CreatedAt = n.CreatedAt
|
||
|
|
})
|
||
|
|
.ToList();
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<WebResult> MarkNotificationsRead(string rootUserId, string avatarId, CancellationToken token)
|
||
|
|
{
|
||
|
|
var result = new WebResult();
|
||
|
|
if (!await Owns(rootUserId, avatarId, token))
|
||
|
|
return result.Invalidate(_localizer["Avatar not found."], StatusCodes.Status404NotFound);
|
||
|
|
await DB.Default.Update<Notification>().Match(n => n.AvatarId == avatarId && !n.IsRead).Modify(n => n.IsRead, true).ExecuteAsync(token);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task<bool> Owns(string rootUserId, string avatarId, CancellationToken token) =>
|
||
|
|
!string.IsNullOrEmpty(rootUserId) && !string.IsNullOrEmpty(avatarId)
|
||
|
|
&& await _dbEntities.RootToAvatars.Match(ra => ra.RootId == rootUserId && ra.AvatarId == avatarId).ExecuteAnyAsync(token);
|
||
|
|
|
||
|
|
static ViewPost ToView(Post post) => new()
|
||
|
|
{
|
||
|
|
Id = post.ID,
|
||
|
|
ObjectURI = post.ObjectURI,
|
||
|
|
AuthorAvatarId = post.IsFederatedCopy ? default : post.GroupUserId,
|
||
|
|
AuthorActorURI = post.ActorURI,
|
||
|
|
GroupId = post.GroupId,
|
||
|
|
DmGroupId = post.ConversationId,
|
||
|
|
Visibility = post.Visibility.ToString(),
|
||
|
|
AnsweringToPostId = post.AnsweringToPostId,
|
||
|
|
Title = post.Title,
|
||
|
|
Text = post.Text,
|
||
|
|
ContentHtml = post.ContentHtml,
|
||
|
|
HasContentWarning = post.HasContentWarning,
|
||
|
|
IsFederatedCopy = post.IsFederatedCopy,
|
||
|
|
CreationDate = post.CreationDate
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|