@ -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,119 @@ | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using BirdsiteLive.ActivityPub; | |||
using BirdsiteLive.Common.Settings; | |||
using Tweetinvi.Models; | |||
using Tweetinvi.Models.Entities; | |||
namespace BirdsiteLive.Domain | |||
{ | |||
public interface IStatusService | |||
{ | |||
Note GetStatus(string username, ITweet tweet); | |||
} | |||
public class StatusService : IStatusService | |||
{ | |||
private readonly InstanceSettings _instanceSettings; | |||
#region Ctor | |||
public StatusService(InstanceSettings instanceSettings) | |||
{ | |||
_instanceSettings = instanceSettings; | |||
} | |||
#endregion | |||
public Note GetStatus(string username, ITweet 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 note = new Note | |||
{ | |||
id = $"{noteId}/activity", | |||
published = tweet.CreatedAt.ToString("s") + "Z", | |||
url = noteUrl, | |||
attributedTo = actorUrl, | |||
//to = new [] {to}, | |||
//cc = new [] { apPublic }, | |||
to = new[] { to }, | |||
cc = new[] { apPublic }, | |||
//cc = new string[0], | |||
sensitive = false, | |||
content = $"<p>{tweet.Text}</p>", | |||
attachment = GetAttachments(tweet.Media), | |||
tag = new string[0] | |||
}; | |||
return note; | |||
} | |||
private Attachment[] GetAttachments(List<IMediaEntity> media) | |||
{ | |||
var result = new List<Attachment>(); | |||
foreach (var m in media) | |||
{ | |||
var mediaUrl = GetMediaUrl(m); | |||
var mediaType = GetMediaType(m.MediaType, mediaUrl); | |||
if (mediaType == null) continue; | |||
var att = new Attachment | |||
{ | |||
type = "Document", | |||
mediaType = mediaType, | |||
url = mediaUrl | |||
}; | |||
result.Add(att); | |||
} | |||
return result.ToArray(); | |||
} | |||
private string GetMediaUrl(IMediaEntity media) | |||
{ | |||
switch (media.MediaType) | |||
{ | |||
case "photo": return media.MediaURLHttps; | |||
case "animated_gif": return media.VideoDetails.Variants[0].URL; | |||
case "video": return media.VideoDetails.Variants.OrderByDescending(x => x.Bitrate).First().URL; | |||
default: return null; | |||
} | |||
} | |||
private string GetMediaType(string mediaType, string mediaUrl) | |||
{ | |||
switch (mediaType) | |||
{ | |||
case "photo": | |||
var ext = Path.GetExtension(mediaUrl); | |||
switch (ext) | |||
{ | |||
case ".jpg": | |||
case ".jpeg": | |||
return "image/jpeg"; | |||
case ".png": | |||
return "image/png"; | |||
} | |||
return null; | |||
case "animated_gif": | |||
return "image/gif"; | |||
case "video": | |||
return "video/mp4"; | |||
} | |||
return null; | |||
} | |||
} | |||
} |
@ -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,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); | |||
} | |||
} | |||
} |
@ -1,60 +1,84 @@ | |||
using System.Linq; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Domain; | |||
using BirdsiteLive.Pipeline.Contracts; | |||
using BirdsiteLive.Pipeline.Models; | |||
using BirdsiteLive.Twitter; | |||
using Tweetinvi.Models; | |||
namespace BirdsiteLive.Pipeline.Processors | |||
{ | |||
public class SendTweetsToFollowersProcessor : ISendTweetsToFollowersProcessor | |||
{ | |||
private readonly IActivityPubService _activityPubService; | |||
private readonly IUserService _userService; | |||
private readonly IStatusService _statusService; | |||
private readonly IFollowersDal _followersDal; | |||
private readonly ITwitterUserDal _twitterUserDal; | |||
#region Ctor | |||
public SendTweetsToFollowersProcessor(IActivityPubService activityPubService, IUserService userService, IFollowersDal followersDal, ITwitterUserDal twitterUserDal) | |||
public SendTweetsToFollowersProcessor(IActivityPubService activityPubService, IFollowersDal followersDal, IStatusService statusService) | |||
{ | |||
_activityPubService = activityPubService; | |||
_userService = userService; | |||
_followersDal = followersDal; | |||
_twitterUserDal = twitterUserDal; | |||
_statusService = statusService; | |||
} | |||
#endregion | |||
public async Task ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct) | |||
public async Task<UserWithTweetsToSync> ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct) | |||
{ | |||
var user = userWithTweetsToSync.User; | |||
var userId = user.Id; | |||
foreach (var follower in userWithTweetsToSync.Followers) | |||
{ | |||
var fromStatusId = follower.FollowingsSyncStatus[userId]; | |||
var tweetsToSend = userWithTweetsToSync.Tweets.Where(x => x.Id > fromStatusId).OrderBy(x => x.Id).ToList(); | |||
try | |||
{ | |||
await ProcessFollowerAsync(userWithTweetsToSync.Tweets, follower, userId, user); | |||
} | |||
catch (Exception e) | |||
{ | |||
Console.WriteLine(e); | |||
//TODO handle error | |||
} | |||
} | |||
return userWithTweetsToSync; | |||
} | |||
private async Task ProcessFollowerAsync(IEnumerable<ITweet> tweets, Follower follower, int userId, | |||
SyncTwitterUser user) | |||
{ | |||
var fromStatusId = follower.FollowingsSyncStatus[userId]; | |||
var tweetsToSend = tweets.Where(x => x.Id > fromStatusId).OrderBy(x => x.Id).ToList(); | |||
var syncStatus = fromStatusId; | |||
var syncStatus = fromStatusId; | |||
try | |||
{ | |||
foreach (var tweet in tweetsToSend) | |||
{ | |||
var note = _userService.GetStatus(user.Acct, tweet); | |||
var result = await _activityPubService.PostNewNoteActivity(note, user.Acct, tweet.Id.ToString(), follower.Host, follower.InboxUrl); | |||
if (result == HttpStatusCode.Accepted) | |||
var note = _statusService.GetStatus(user.Acct, tweet); | |||
var result = await _activityPubService.PostNewNoteActivity(note, user.Acct, tweet.Id.ToString(), follower.Host, | |||
follower.InboxUrl); | |||
if (result == HttpStatusCode.Accepted) | |||
syncStatus = tweet.Id; | |||
else | |||
break; | |||
throw new Exception("Posting new note activity failed"); | |||
} | |||
} | |||
finally | |||
{ | |||
if (syncStatus != fromStatusId) | |||
{ | |||
follower.FollowingsSyncStatus[userId] = syncStatus; | |||
await _followersDal.UpdateFollowerAsync(follower); | |||
} | |||
follower.FollowingsSyncStatus[userId] = syncStatus; | |||
await _followersDal.UpdateFollowerAsync(follower); | |||
} | |||
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); | |||
} | |||
} | |||
} |