@ -0,0 +1,39 @@ | |||
version: "3" | |||
networks: | |||
birdsitelivenetwork: | |||
external: false | |||
services: | |||
server: | |||
image: nicolasconstant/birdsitelive:latest | |||
restart: always | |||
container_name: birdsitelive | |||
environment: | |||
- Instance:Domain=domain.name | |||
- Instance:AdminEmail=name@domain.ext | |||
- Db:Type=postgres | |||
- Db:Host=db | |||
- Db:Name=birdsitelive | |||
- Db:User=birdsitelive | |||
- Db:Password=birdsitelive | |||
- Twitter:ConsumerKey=twitter.api.key | |||
- Twitter:ConsumerSecret=twitter.api.key | |||
networks: | |||
- birdsitelivenetwork | |||
ports: | |||
- "5000:80" | |||
depends_on: | |||
- db | |||
db: | |||
image: postgres:9.6 | |||
restart: always | |||
environment: | |||
- POSTGRES_USER=birdsitelive | |||
- POSTGRES_PASSWORD=birdsitelive | |||
- POSTGRES_DB=birdsitelive | |||
networks: | |||
- birdsitelivenetwork | |||
volumes: | |||
- ./postgres:/var/lib/postgresql/data |
@ -0,0 +1,10 @@ | |||
using Newtonsoft.Json; | |||
namespace BirdsiteLive.ActivityPub | |||
{ | |||
public class ActivityAcceptUndoFollow : Activity | |||
{ | |||
[JsonProperty("object")] | |||
public ActivityUndoFollow apObject { get; set; } | |||
} | |||
} |
@ -0,0 +1,9 @@ | |||
namespace BirdsiteLive.ActivityPub | |||
{ | |||
public class Attachment | |||
{ | |||
public string type { get; set; } | |||
public string mediaType { get; set; } | |||
public string url { get; set; } | |||
} | |||
} |
@ -0,0 +1,7 @@ | |||
namespace BirdsiteLive.ActivityPub | |||
{ | |||
public class EndPoints | |||
{ | |||
public string sharedInbox { get; set; } | |||
} | |||
} |
@ -0,0 +1,15 @@ | |||
using BirdsiteLive.ActivityPub.Converters; | |||
using Newtonsoft.Json; | |||
namespace BirdsiteLive.ActivityPub.Models | |||
{ | |||
public class Followers | |||
{ | |||
[JsonProperty("@context")] | |||
[JsonConverter(typeof(ContextArrayConverter))] | |||
public string context { get; set; } = "https://www.w3.org/ns/activitystreams"; | |||
public string id { get; set; } | |||
public string type { get; set; } = "OrderedCollection"; | |||
} | |||
} |
@ -0,0 +1,8 @@ | |||
namespace BirdsiteLive.ActivityPub.Models | |||
{ | |||
public class Tag { | |||
public string type { get; set; } //Hashtag | |||
public string href { get; set; } //https://mastodon.social/tags/app | |||
public string name { get; set; } //#app | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
namespace BirdsiteLive.Common.Settings | |||
{ | |||
public class DbSettings | |||
{ | |||
public string Type { get; set; } | |||
public string Host { get; set; } | |||
public string Name { get; set; } | |||
public string User { get; set; } | |||
public string Password { get; set; } | |||
} | |||
} |
@ -0,0 +1,7 @@ | |||
namespace BirdsiteLive.Common.Structs | |||
{ | |||
public struct DbTypes | |||
{ | |||
public static string Postgres = "postgres"; | |||
} | |||
} |
@ -0,0 +1,53 @@ | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
namespace BirdsiteLive.Domain.BusinessUseCases | |||
{ | |||
public interface IProcessFollowUser | |||
{ | |||
Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername, string followerInbox, string sharedInbox); | |||
} | |||
public class ProcessFollowUser : IProcessFollowUser | |||
{ | |||
private readonly IFollowersDal _followerDal; | |||
private readonly ITwitterUserDal _twitterUserDal; | |||
#region Ctor | |||
public ProcessFollowUser(IFollowersDal followerDal, ITwitterUserDal twitterUserDal) | |||
{ | |||
_followerDal = followerDal; | |||
_twitterUserDal = twitterUserDal; | |||
} | |||
#endregion | |||
public async Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername, string followerInbox, string sharedInbox) | |||
{ | |||
// Get Follower and Twitter Users | |||
var follower = await _followerDal.GetFollowerAsync(followerUsername, followerDomain); | |||
if (follower == null) | |||
{ | |||
await _followerDal.CreateFollowerAsync(followerUsername, followerDomain, followerInbox, sharedInbox); | |||
follower = await _followerDal.GetFollowerAsync(followerUsername, followerDomain); | |||
} | |||
var twitterUser = await _twitterUserDal.GetTwitterUserAsync(twitterUsername); | |||
if (twitterUser == null) | |||
{ | |||
await _twitterUserDal.CreateTwitterUserAsync(twitterUsername, -1); | |||
twitterUser = await _twitterUserDal.GetTwitterUserAsync(twitterUsername); | |||
} | |||
// Update Follower | |||
var twitterUserId = twitterUser.Id; | |||
if(!follower.Followings.Contains(twitterUserId)) | |||
follower.Followings.Add(twitterUserId); | |||
if(!follower.FollowingsSyncStatus.ContainsKey(twitterUserId)) | |||
follower.FollowingsSyncStatus.Add(twitterUserId, -1); | |||
// Save Follower | |||
await _followerDal.UpdateFollowerAsync(follower); | |||
} | |||
} | |||
} |
@ -0,0 +1,45 @@ | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
namespace BirdsiteLive.Domain.BusinessUseCases | |||
{ | |||
public interface IProcessUndoFollowUser | |||
{ | |||
Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername); | |||
} | |||
public class ProcessUndoFollowUser : IProcessUndoFollowUser | |||
{ | |||
private readonly IFollowersDal _followerDal; | |||
private readonly ITwitterUserDal _twitterUserDal; | |||
#region Ctor | |||
public ProcessUndoFollowUser(IFollowersDal followerDal, ITwitterUserDal twitterUserDal) | |||
{ | |||
_followerDal = followerDal; | |||
_twitterUserDal = twitterUserDal; | |||
} | |||
#endregion | |||
public async Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername) | |||
{ | |||
// Get Follower and Twitter Users | |||
var follower = await _followerDal.GetFollowerAsync(followerUsername, followerDomain); | |||
if (follower == null) return; | |||
var twitterUser = await _twitterUserDal.GetTwitterUserAsync(twitterUsername); | |||
if (twitterUser == null) return; | |||
// Update Follower | |||
var twitterUserId = twitterUser.Id; | |||
if (follower.Followings.Contains(twitterUserId)) | |||
follower.Followings.Remove(twitterUserId); | |||
if (follower.FollowingsSyncStatus.ContainsKey(twitterUserId)) | |||
follower.FollowingsSyncStatus.Remove(twitterUserId); | |||
// Save Follower | |||
await _followerDal.UpdateFollowerAsync(follower); | |||
} | |||
} | |||
} |
@ -0,0 +1,90 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Text.RegularExpressions; | |||
using BirdsiteLive.ActivityPub; | |||
using BirdsiteLive.ActivityPub.Models; | |||
using BirdsiteLive.Common.Settings; | |||
using BirdsiteLive.Domain.Tools; | |||
using BirdsiteLive.Twitter.Models; | |||
using Tweetinvi.Models; | |||
using Tweetinvi.Models.Entities; | |||
namespace BirdsiteLive.Domain | |||
{ | |||
public interface IStatusService | |||
{ | |||
Note GetStatus(string username, ExtractedTweet tweet); | |||
} | |||
public class StatusService : IStatusService | |||
{ | |||
private readonly InstanceSettings _instanceSettings; | |||
private readonly IStatusExtractor _statusExtractor; | |||
#region Ctor | |||
public StatusService(InstanceSettings instanceSettings, IStatusExtractor statusExtractor) | |||
{ | |||
_instanceSettings = instanceSettings; | |||
_statusExtractor = statusExtractor; | |||
} | |||
#endregion | |||
public Note GetStatus(string username, ExtractedTweet tweet) | |||
{ | |||
var actorUrl = $"https://{_instanceSettings.Domain}/users/{username}"; | |||
var noteId = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{tweet.Id}"; | |||
var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{tweet.Id}"; | |||
var to = $"{actorUrl}/followers"; | |||
var apPublic = "https://www.w3.org/ns/activitystreams#Public"; | |||
var extractedTags = _statusExtractor.ExtractTags(tweet.MessageContent); | |||
string inReplyTo = null; | |||
if (tweet.InReplyToStatusId != default) | |||
inReplyTo = $"https://{_instanceSettings.Domain}/users/{tweet.InReplyToAccount}/statuses/{tweet.InReplyToStatusId}"; | |||
var note = new Note | |||
{ | |||
//id = $"{noteId}/activity", | |||
id = $"{noteId}", | |||
published = tweet.CreatedAt.ToString("s") + "Z", | |||
url = noteUrl, | |||
attributedTo = actorUrl, | |||
inReplyTo = inReplyTo, | |||
//to = new [] {to}, | |||
//cc = new [] { apPublic }, | |||
to = new[] { to }, | |||
//cc = new[] { apPublic }, | |||
cc = new string[0], | |||
sensitive = false, | |||
content = $"<p>{extractedTags.content}</p>", | |||
attachment = Convert(tweet.Media), | |||
tag = extractedTags.tags | |||
}; | |||
return note; | |||
} | |||
private Attachment[] Convert(ExtractedMedia[] media) | |||
{ | |||
if(media == null) return new Attachment[0]; | |||
return media.Select(x => | |||
{ | |||
return new Attachment | |||
{ | |||
type = "Document", | |||
url = x.Url, | |||
mediaType = x.MediaType | |||
}; | |||
}).ToArray(); | |||
} | |||
} | |||
} |
@ -0,0 +1,130 @@ | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text.RegularExpressions; | |||
using BirdsiteLive.ActivityPub.Models; | |||
using BirdsiteLive.Common.Settings; | |||
namespace BirdsiteLive.Domain.Tools | |||
{ | |||
public interface IStatusExtractor | |||
{ | |||
(string content, Tag[] tags) ExtractTags(string messageContent); | |||
} | |||
public class StatusExtractor : IStatusExtractor | |||
{ | |||
private readonly Regex _hastagRegex = new Regex(@"\W(\#[a-zA-Z0-9_ー]+\b)(?!;)"); | |||
//private readonly Regex _hastagRegex = new Regex(@"#\w+"); | |||
//private readonly Regex _hastagRegex = new Regex(@"(?<=[\s>]|^)#(\w*[a-zA-Z0-9_ー]+\w*)\b(?!;)"); | |||
//private readonly Regex _hastagRegex = new Regex(@"(?<=[\s>]|^)#(\w*[a-zA-Z0-9_ー]+)\b(?!;)"); | |||
private readonly Regex _mentionRegex = new Regex(@"\W(\@[a-zA-Z0-9_ー]+\b)(?!;)"); | |||
//private readonly Regex _mentionRegex = new Regex(@"@\w+"); | |||
//private readonly Regex _mentionRegex = new Regex(@"(?<=[\s>]|^)@(\w*[a-zA-Z0-9_ー]+\w*)\b(?!;)"); | |||
//private readonly Regex _mentionRegex = new Regex(@"(?<=[\s>]|^)@(\w*[a-zA-Z0-9_ー]+)\b(?!;)"); | |||
private readonly Regex _urlRegex = new Regex(@"((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?)"); | |||
private readonly InstanceSettings _instanceSettings; | |||
#region Ctor | |||
public StatusExtractor(InstanceSettings instanceSettings) | |||
{ | |||
_instanceSettings = instanceSettings; | |||
} | |||
#endregion | |||
public (string content, Tag[] tags) ExtractTags(string messageContent) | |||
{ | |||
var tags = new List<Tag>(); | |||
messageContent = $" {messageContent} "; | |||
// Replace return lines | |||
messageContent = Regex.Replace(messageContent, @"\r\n\r\n?|\n\n", "</p><p> "); | |||
messageContent = Regex.Replace(messageContent, @"\r\n?|\n", "<br/> "); | |||
// Extract Urls | |||
var urlMatch = _urlRegex.Matches(messageContent); | |||
foreach (Match m in urlMatch) | |||
{ | |||
var url = m.ToString().Replace("\n", string.Empty).Trim(); | |||
var protocol = "https://"; | |||
if (url.StartsWith("http://")) protocol = "http://"; | |||
else if (url.StartsWith("ftp://")) protocol = "ftp://"; | |||
var truncatedUrl = url.Replace(protocol, string.Empty); | |||
if (truncatedUrl.StartsWith("www.")) | |||
{ | |||
protocol += "www."; | |||
truncatedUrl = truncatedUrl.Replace("www.", string.Empty); | |||
} | |||
var firstPart = truncatedUrl; | |||
var secondPart = string.Empty; | |||
if (truncatedUrl.Length > 30) | |||
{ | |||
firstPart = truncatedUrl.Substring(0, 30); | |||
secondPart = truncatedUrl.Substring(30); | |||
} | |||
messageContent = Regex.Replace(messageContent, m.ToString(), | |||
$@" <a href=""{url}"" rel=""nofollow noopener noreferrer"" target=""_blank""><span class=""invisible"">{protocol}</span><span class=""ellipsis"">{firstPart}</span><span class=""invisible"">{secondPart}</span></a>"); | |||
} | |||
// Extract Hashtags | |||
var hashtagMatch = OrderByLength(_hastagRegex.Matches(messageContent)); | |||
foreach (Match m in hashtagMatch) | |||
{ | |||
var tag = m.ToString().Replace("#", string.Empty).Replace("\n", string.Empty).Trim(); | |||
var url = $"https://{_instanceSettings.Domain}/tags/{tag}"; | |||
tags.Add(new Tag | |||
{ | |||
name = $"#{tag}", | |||
href = url, | |||
type = "Hashtag" | |||
}); | |||
messageContent = Regex.Replace(messageContent, m.ToString(), | |||
$@" <a href=""{url}"" class=""mention hashtag"" rel=""tag"">#<span>{tag}</span></a>"); | |||
} | |||
// Extract Mentions | |||
var mentionMatch = OrderByLength(_mentionRegex.Matches(messageContent)); | |||
foreach (Match m in mentionMatch) | |||
{ | |||
var mention = m.ToString().Replace("@", string.Empty).Replace("\n", string.Empty).Trim(); | |||
var url = $"https://{_instanceSettings.Domain}/users/{mention}"; | |||
var name = $"@{mention}@{_instanceSettings.Domain}"; | |||
tags.Add(new Tag | |||
{ | |||
name = name, | |||
href = url, | |||
type = "Mention" | |||
}); | |||
messageContent = Regex.Replace(messageContent, m.ToString(), | |||
$@" <span class=""h-card""><a href=""https://{_instanceSettings.Domain}/@{mention}"" class=""u-url mention"">@<span>{mention}</span></a></span>"); | |||
} | |||
// Clean up return lines | |||
messageContent = Regex.Replace(messageContent, @"<p> ", "<p>"); | |||
messageContent = Regex.Replace(messageContent, @"<br/> ", "<br/>"); | |||
return (messageContent.Trim(), tags.ToArray()); | |||
} | |||
private IEnumerable<Match> OrderByLength(MatchCollection matches) | |||
{ | |||
var result = new List<Match>(); | |||
foreach (Match m in matches) result.Add(m); | |||
result = result.OrderByDescending(x => x.Length).ToList(); | |||
return result; | |||
} | |||
} | |||
} |
@ -0,0 +1,19 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netstandard2.0</TargetFramework> | |||
<LangVersion>latest</LangVersion> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="1.1.1" /> | |||
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.11.1" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\BirdsiteLive.Domain\BirdsiteLive.Domain.csproj" /> | |||
<ProjectReference Include="..\BirdsiteLive.Twitter\BirdsiteLive.Twitter.csproj" /> | |||
<ProjectReference Include="..\DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,13 @@ | |||
using System.Collections.Generic; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.Pipeline.Models; | |||
namespace BirdsiteLive.Pipeline.Contracts | |||
{ | |||
public interface IRetrieveFollowersProcessor | |||
{ | |||
Task<IEnumerable<UserWithTweetsToSync>> ProcessAsync(UserWithTweetsToSync[] userWithTweetsToSyncs, CancellationToken ct); | |||
//IAsyncEnumerable<UserWithTweetsToSync> ProcessAsync(UserWithTweetsToSync[] userWithTweetsToSyncs, CancellationToken ct); | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Pipeline.Models; | |||
namespace BirdsiteLive.Pipeline.Contracts | |||
{ | |||
public interface IRetrieveTweetsProcessor | |||
{ | |||
Task<UserWithTweetsToSync[]> ProcessAsync(SyncTwitterUser[] syncTwitterUsers, CancellationToken ct); | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using System.Threading.Tasks.Dataflow; | |||
using BirdsiteLive.DAL.Models; | |||
namespace BirdsiteLive.Pipeline.Contracts | |||
{ | |||
public interface IRetrieveTwitterUsersProcessor | |||
{ | |||
Task GetTwitterUsersAsync(BufferBlock<SyncTwitterUser[]> twitterUsersBufferBlock, CancellationToken ct); | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.Pipeline.Models; | |||
namespace BirdsiteLive.Pipeline.Contracts | |||
{ | |||
public interface ISaveProgressionProcessor | |||
{ | |||
Task ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct); | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.Pipeline.Models; | |||
namespace BirdsiteLive.Pipeline.Contracts | |||
{ | |||
public interface ISendTweetsToFollowersProcessor | |||
{ | |||
Task<UserWithTweetsToSync> ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct); | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Twitter.Models; | |||
using Tweetinvi.Models; | |||
namespace BirdsiteLive.Pipeline.Models | |||
{ | |||
public class UserWithTweetsToSync | |||
{ | |||
public SyncTwitterUser User { get; set; } | |||
public ExtractedTweet[] Tweets { get; set; } | |||
public Follower[] Followers { get; set; } | |||
} | |||
} |
@ -0,0 +1,33 @@ | |||
using System.Collections.Generic; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.Pipeline.Contracts; | |||
using BirdsiteLive.Pipeline.Models; | |||
namespace BirdsiteLive.Pipeline.Processors | |||
{ | |||
public class RetrieveFollowersProcessor : IRetrieveFollowersProcessor | |||
{ | |||
private readonly IFollowersDal _followersDal; | |||
#region Ctor | |||
public RetrieveFollowersProcessor(IFollowersDal followersDal) | |||
{ | |||
_followersDal = followersDal; | |||
} | |||
#endregion | |||
public async Task<IEnumerable<UserWithTweetsToSync>> ProcessAsync(UserWithTweetsToSync[] userWithTweetsToSyncs, CancellationToken ct) | |||
{ | |||
//TODO multithread this | |||
foreach (var user in userWithTweetsToSyncs) | |||
{ | |||
var followers = await _followersDal.GetFollowersAsync(user.User.Id); | |||
user.Followers = followers; | |||
} | |||
return userWithTweetsToSyncs; | |||
} | |||
} | |||
} |
@ -0,0 +1,66 @@ | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Pipeline.Contracts; | |||
using BirdsiteLive.Pipeline.Models; | |||
using BirdsiteLive.Twitter; | |||
using BirdsiteLive.Twitter.Models; | |||
using Tweetinvi.Models; | |||
namespace BirdsiteLive.Pipeline.Processors | |||
{ | |||
public class RetrieveTweetsProcessor : IRetrieveTweetsProcessor | |||
{ | |||
private readonly ITwitterService _twitterService; | |||
private readonly ITwitterUserDal _twitterUserDal; | |||
#region Ctor | |||
public RetrieveTweetsProcessor(ITwitterService twitterService, ITwitterUserDal twitterUserDal) | |||
{ | |||
_twitterService = twitterService; | |||
_twitterUserDal = twitterUserDal; | |||
} | |||
#endregion | |||
public async Task<UserWithTweetsToSync[]> ProcessAsync(SyncTwitterUser[] syncTwitterUsers, CancellationToken ct) | |||
{ | |||
var usersWtTweets = new List<UserWithTweetsToSync>(); | |||
//TODO multithread this | |||
foreach (var user in syncTwitterUsers) | |||
{ | |||
var tweets = RetrieveNewTweets(user); | |||
if (tweets.Length > 0 && user.LastTweetPostedId != -1) | |||
{ | |||
var userWtTweets = new UserWithTweetsToSync | |||
{ | |||
User = user, | |||
Tweets = tweets | |||
}; | |||
usersWtTweets.Add(userWtTweets); | |||
} | |||
else if (tweets.Length > 0 && user.LastTweetPostedId == -1) | |||
{ | |||
var tweetId = tweets.Last().Id; | |||
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, tweetId, tweetId); | |||
} | |||
} | |||
return usersWtTweets.ToArray(); | |||
} | |||
private ExtractedTweet[] RetrieveNewTweets(SyncTwitterUser user) | |||
{ | |||
ExtractedTweet[] tweets; | |||
if (user.LastTweetPostedId == -1) | |||
tweets = _twitterService.GetTimeline(user.Acct, 1); | |||
else | |||
tweets = _twitterService.GetTimeline(user.Acct, 200, user.LastTweetSynchronizedForAllFollowersId); | |||
return tweets; | |||
} | |||
} | |||
} |
@ -0,0 +1,45 @@ | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using System.Threading.Tasks.Dataflow; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Pipeline.Contracts; | |||
namespace BirdsiteLive.Pipeline.Processors | |||
{ | |||
public class RetrieveTwitterUsersProcessor : IRetrieveTwitterUsersProcessor | |||
{ | |||
private readonly ITwitterUserDal _twitterUserDal; | |||
private const int SyncPeriod = 15; //in minutes | |||
#region Ctor | |||
public RetrieveTwitterUsersProcessor(ITwitterUserDal twitterUserDal) | |||
{ | |||
_twitterUserDal = twitterUserDal; | |||
} | |||
#endregion | |||
public async Task GetTwitterUsersAsync(BufferBlock<SyncTwitterUser[]> twitterUsersBufferBlock, CancellationToken ct) | |||
{ | |||
for (;;) | |||
{ | |||
ct.ThrowIfCancellationRequested(); | |||
try | |||
{ | |||
var users = await _twitterUserDal.GetAllTwitterUsersAsync(); | |||
if(users.Length > 0) | |||
await twitterUsersBufferBlock.SendAsync(users, ct); | |||
} | |||
catch (Exception e) | |||
{ | |||
Console.WriteLine(e); | |||
} | |||
await Task.Delay(SyncPeriod * 1000 * 60, ct); | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,29 @@ | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.Pipeline.Contracts; | |||
using BirdsiteLive.Pipeline.Models; | |||
namespace BirdsiteLive.Pipeline.Processors | |||
{ | |||
public class SaveProgressionProcessor : ISaveProgressionProcessor | |||
{ | |||
private readonly ITwitterUserDal _twitterUserDal; | |||
#region Ctor | |||
public SaveProgressionProcessor(ITwitterUserDal twitterUserDal) | |||
{ | |||
_twitterUserDal = twitterUserDal; | |||
} | |||
#endregion | |||
public async Task ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct) | |||
{ | |||
var userId = userWithTweetsToSync.User.Id; | |||
var lastPostedTweet = userWithTweetsToSync.Tweets.Select(x => x.Id).Max(); | |||
var minimumSync = userWithTweetsToSync.Followers.Select(x => x.FollowingsSyncStatus[userId]).Min(); | |||
await _twitterUserDal.UpdateTwitterUserAsync(userId, lastPostedTweet, minimumSync); | |||
} | |||
} | |||
} |
@ -0,0 +1,86 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using System.Xml; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Domain; | |||
using BirdsiteLive.Pipeline.Contracts; | |||
using BirdsiteLive.Pipeline.Models; | |||
using BirdsiteLive.Pipeline.Processors.SubTasks; | |||
using BirdsiteLive.Twitter; | |||
using BirdsiteLive.Twitter.Models; | |||
using Tweetinvi.Models; | |||
namespace BirdsiteLive.Pipeline.Processors | |||
{ | |||
public class SendTweetsToFollowersProcessor : ISendTweetsToFollowersProcessor | |||
{ | |||
private readonly ISendTweetsToInboxTask _sendTweetsToInboxTask; | |||
private readonly ISendTweetsToSharedInboxTask _sendTweetsToSharedInbox; | |||
#region Ctor | |||
public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox) | |||
{ | |||
_sendTweetsToInboxTask = sendTweetsToInboxTask; | |||
_sendTweetsToSharedInbox = sendTweetsToSharedInbox; | |||
} | |||
#endregion | |||
public async Task<UserWithTweetsToSync> ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct) | |||
{ | |||
var user = userWithTweetsToSync.User; | |||
// Process Shared Inbox | |||
var followersWtSharedInbox = userWithTweetsToSync.Followers | |||
.Where(x => !string.IsNullOrWhiteSpace(x.SharedInboxRoute)) | |||
.ToList(); | |||
await ProcessFollowersWithSharedInbox(userWithTweetsToSync.Tweets, followersWtSharedInbox, user); | |||
// Process Inbox | |||
var followerWtInbox = userWithTweetsToSync.Followers | |||
.Where(x => string.IsNullOrWhiteSpace(x.SharedInboxRoute)) | |||
.ToList(); | |||
await ProcessFollowersWithInbox(userWithTweetsToSync.Tweets, followerWtInbox, user); | |||
return userWithTweetsToSync; | |||
} | |||
private async Task ProcessFollowersWithSharedInbox(ExtractedTweet[] tweets, List<Follower> followers, SyncTwitterUser user) | |||
{ | |||
var followersPerInstances = followers.GroupBy(x => x.Host); | |||
foreach (var followersPerInstance in followersPerInstances) | |||
{ | |||
try | |||
{ | |||
await _sendTweetsToSharedInbox.ExecuteAsync(tweets, user, followersPerInstance.Key, followersPerInstance.ToArray()); | |||
} | |||
catch (Exception e) | |||
{ | |||
Console.WriteLine(e); | |||
//TODO handle error | |||
} | |||
} | |||
} | |||
private async Task ProcessFollowersWithInbox(ExtractedTweet[] tweets, List<Follower> followerWtInbox, SyncTwitterUser user) | |||
{ | |||
foreach (var follower in followerWtInbox) | |||
{ | |||
try | |||
{ | |||
await _sendTweetsToInboxTask.ExecuteAsync(tweets, follower, user); | |||
} | |||
catch (Exception e) | |||
{ | |||
Console.WriteLine(e); | |||
//TODO handle error | |||
} | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,68 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Domain; | |||
using BirdsiteLive.Twitter.Models; | |||
namespace BirdsiteLive.Pipeline.Processors.SubTasks | |||
{ | |||
public interface ISendTweetsToInboxTask | |||
{ | |||
Task ExecuteAsync(IEnumerable<ExtractedTweet> tweets, Follower follower, SyncTwitterUser user); | |||
} | |||
public class SendTweetsToInboxTask : ISendTweetsToInboxTask | |||
{ | |||
private readonly IActivityPubService _activityPubService; | |||
private readonly IStatusService _statusService; | |||
private readonly IFollowersDal _followersDal; | |||
#region Ctor | |||
public SendTweetsToInboxTask(IActivityPubService activityPubService, IStatusService statusService, IFollowersDal followersDal) | |||
{ | |||
_activityPubService = activityPubService; | |||
_statusService = statusService; | |||
_followersDal = followersDal; | |||
} | |||
#endregion | |||
public async Task ExecuteAsync(IEnumerable<ExtractedTweet> tweets, Follower follower, SyncTwitterUser user) | |||
{ | |||
var userId = user.Id; | |||
var fromStatusId = follower.FollowingsSyncStatus[userId]; | |||
var tweetsToSend = tweets | |||
.Where(x => x.Id > fromStatusId) | |||
.OrderBy(x => x.Id) | |||
.ToList(); | |||
var inbox = follower.InboxRoute; | |||
var syncStatus = fromStatusId; | |||
try | |||
{ | |||
foreach (var tweet in tweetsToSend) | |||
{ | |||
var note = _statusService.GetStatus(user.Acct, tweet); | |||
var result = await _activityPubService.PostNewNoteActivity(note, user.Acct, tweet.Id.ToString(), follower.Host, inbox); | |||
if (result == HttpStatusCode.Accepted) | |||
syncStatus = tweet.Id; | |||
else | |||
throw new Exception("Posting new note activity failed"); | |||
} | |||
} | |||
finally | |||
{ | |||
if (syncStatus != fromStatusId) | |||
{ | |||
follower.FollowingsSyncStatus[userId] = syncStatus; | |||
await _followersDal.UpdateFollowerAsync(follower); | |||
} | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,73 @@ | |||
using System; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Domain; | |||
using BirdsiteLive.Twitter.Models; | |||
namespace BirdsiteLive.Pipeline.Processors.SubTasks | |||
{ | |||
public interface ISendTweetsToSharedInboxTask | |||
{ | |||
Task ExecuteAsync(ExtractedTweet[] tweets, SyncTwitterUser user, string host, Follower[] followersPerInstance); | |||
} | |||
public class SendTweetsToSharedInboxTask : ISendTweetsToSharedInboxTask | |||
{ | |||
private readonly IStatusService _statusService; | |||
private readonly IActivityPubService _activityPubService; | |||
private readonly IFollowersDal _followersDal; | |||
#region Ctor | |||
public SendTweetsToSharedInboxTask(IActivityPubService activityPubService, IStatusService statusService, IFollowersDal followersDal) | |||
{ | |||
_activityPubService = activityPubService; | |||
_statusService = statusService; | |||
_followersDal = followersDal; | |||
} | |||
#endregion | |||
public async Task ExecuteAsync(ExtractedTweet[] tweets, SyncTwitterUser user, string host, Follower[] followersPerInstance) | |||
{ | |||