using MongoDB.Driver; using MongoDB.Entities; using PrivaPub.Domain.Social; using PrivaPub.Models.Group; using PrivaPub.Models.Post; using PrivaPub.Models.Social; using PrivaPub.StaticServices; using GroupEntity = PrivaPub.Models.Group.Group; namespace PrivaPub.Domain.Timelines { public interface IFanout { Task Distribute(Post post, CancellationToken token); } public class Fanout : IFanout { readonly DbEntities _dbEntities; public Fanout(DbEntities dbEntities) { _dbEntities = dbEntities; } public async Task Distribute(Post post, CancellationToken token) { var recipients = new HashSet(StringComparer.Ordinal); if (!post.IsFederatedCopy && !string.IsNullOrEmpty(post.GroupUserId)) recipients.Add(post.GroupUserId); switch (post.Visibility) { case PostVisibility.Public or PostVisibility.Unlisted or PostVisibility.FollowersOnly: var followers = post.IsFederatedCopy ? await _dbEntities.Followings.Match(f => f.TargetActorURI == post.ActorURI && f.State == FollowState.Accepted).ExecuteAsync(token) : await _dbEntities.Followings.Match(f => f.TargetAccountId == post.GroupUserId && f.TargetIsLocal && f.State == FollowState.Accepted).ExecuteAsync(token); foreach (var following in followers) if (await Shows(following, post, token)) recipients.Add(following.AvatarId); break; case PostVisibility.Direct when !string.IsNullOrEmpty(post.ConversationId): var conversation = await _dbEntities.DmGroups.MatchID(post.ConversationId).ExecuteFirstAsync(token); foreach (var member in conversation?.Members.Where(m => !m.IsForeign) ?? Enumerable.Empty()) recipients.Add(member.AvatarId); break; case PostVisibility.Circle when !string.IsNullOrEmpty(post.GroupId): var circle = await DB.Default.Find().MatchID(post.GroupId).ExecuteFirstAsync(token); foreach (var member in circle?.Members.Where(m => !m.IsForeign) ?? Enumerable.Empty()) recipients.Add(member.AvatarId); break; } recipients.ExceptWith(await Relationships.Hidden.RecipientsHiding(recipients, post.ActorURI, token)); foreach (var avatarId in recipients) { try { await DB.Default.SaveAsync(new TimelineEntry { AvatarId = avatarId, PostId = post.ID, AuthorAccountId = post.AuthorAccountId ?? post.GroupUserId, ReblogOfPostId = post.ReblogOfPostId }, token); } catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { } } if (string.IsNullOrEmpty(post.ReblogOfPostId)) foreach (var mention in post.Mentions.Where(m => m.IsLocal && !string.IsNullOrEmpty(m.AccountId))) await Notifications.Add(mention.AccountId, NotificationType.Mention, post.AuthorAccountId ?? post.GroupUserId, post.ActorURI, post.ID, token); } async Task Shows(Following following, Post post, CancellationToken token) { if (!string.IsNullOrEmpty(post.ReblogOfPostId)) return following.ShowReblogs; var repliedTo = post.InReplyToAccountId; var author = post.AuthorAccountId ?? post.GroupUserId; if (string.IsNullOrEmpty(post.InReplyToURI) || repliedTo == author || repliedTo == following.AvatarId) return true; if (string.IsNullOrEmpty(repliedTo)) return false; return await _dbEntities.Followings .Match(f => f.AvatarId == following.AvatarId && f.TargetAccountId == repliedTo && f.State == FollowState.Accepted) .ExecuteAnyAsync(token); } } }