support attachments
This commit is contained in:
parent
1bcb00cdd5
commit
10104187d5
10 changed files with 226 additions and 66 deletions
9
src/BirdsiteLive.ActivityPub/Models/Attachment.cs
Normal file
9
src/BirdsiteLive.ActivityPub/Models/Attachment.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ namespace BirdsiteLive.ActivityPub
|
|||
//public string conversation { get; set; }
|
||||
public string content { get; set; }
|
||||
//public Dictionary<string,string> contentMap { get; set; }
|
||||
public string[] attachment { get; set; }
|
||||
public Attachment[] attachment { get; set; }
|
||||
public string[] tag { get; set; }
|
||||
//public Dictionary<string, string> replies;
|
||||
}
|
||||
|
|
119
src/BirdsiteLive.Domain/StatusService.cs
Normal file
119
src/BirdsiteLive.Domain/StatusService.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,8 +18,6 @@ namespace BirdsiteLive.Domain
|
|||
public interface IUserService
|
||||
{
|
||||
Actor GetUser(TwitterUser twitterUser);
|
||||
//Note GetStatus(TwitterUser user, ITweet tweet);
|
||||
Note GetStatus(string username, ITweet tweet);
|
||||
Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity);
|
||||
Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityUndoFollow activity);
|
||||
}
|
||||
|
@ -75,41 +73,6 @@ namespace BirdsiteLive.Domain
|
|||
return user;
|
||||
}
|
||||
|
||||
public Note GetStatus(string username, ITweet tweet)
|
||||
{
|
||||
//var actor = GetUser(user);
|
||||
|
||||
var actorUrl = $"{_host}/users/{username}";
|
||||
var noteId = $"{_host}/users/{username}/statuses/{tweet.Id}";
|
||||
var noteUrl = $"{_host}/@{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[] { apPublic },
|
||||
cc = new[] { to },
|
||||
|
||||
sensitive = false,
|
||||
content = $"<p>{tweet.Text}</p>",
|
||||
attachment = new string[0],
|
||||
tag = new string[0]
|
||||
};
|
||||
return note;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity)
|
||||
{
|
||||
// Validate
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,6 @@ namespace BirdsiteLive.Pipeline.Contracts
|
|||
{
|
||||
public interface ISendTweetsToFollowersProcessor
|
||||
{
|
||||
Task ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct);
|
||||
Task<UserWithTweetsToSync> 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();
|
||||
|
||||
var syncStatus = fromStatusId;
|
||||
foreach (var tweet in tweetsToSend)
|
||||
try
|
||||
{
|
||||
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)
|
||||
syncStatus = tweet.Id;
|
||||
else
|
||||
break;
|
||||
await ProcessFollowerAsync(userWithTweetsToSync.Tweets, follower, userId, user);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
//TODO handle error
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
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,
|
||||
follower.InboxUrl);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -92,7 +92,7 @@ namespace BirdsiteLive.Controllers
|
|||
//cc = new [] { apPublic },
|
||||
sensitive = false,
|
||||
content = "<p>Woooot</p>",
|
||||
attachment = new string[0],
|
||||
attachment = new Attachment[0],
|
||||
tag = new string[0]
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.ActivityPub;
|
||||
|
@ -18,12 +19,14 @@ namespace BirdsiteLive.Controllers
|
|||
{
|
||||
private readonly ITwitterService _twitterService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IStatusService _statusService;
|
||||
|
||||
#region Ctor
|
||||
public UsersController(ITwitterService twitterService, IUserService userService)
|
||||
public UsersController(ITwitterService twitterService, IUserService userService, IStatusService statusService)
|
||||
{
|
||||
_twitterService = twitterService;
|
||||
_userService = userService;
|
||||
_statusService = statusService;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
@ -62,7 +65,7 @@ namespace BirdsiteLive.Controllers
|
|||
//var user = _twitterService.GetUser(id);
|
||||
//if (user == null) return NotFound();
|
||||
|
||||
var status = _userService.GetStatus(id, tweet);
|
||||
var status = _statusService.GetStatus(id, tweet);
|
||||
var jsonApUser = JsonConvert.SerializeObject(status);
|
||||
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
||||
}
|
||||
|
@ -78,6 +81,8 @@ namespace BirdsiteLive.Controllers
|
|||
using (var reader = new StreamReader(Request.Body))
|
||||
{
|
||||
var body = await reader.ReadToEndAsync();
|
||||
//System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body);
|
||||
|
||||
var activity = ApDeserializer.ProcessActivity(body);
|
||||
// Do something
|
||||
var signature = r.Headers["Signature"].First();
|
||||
|
|
Reference in a new issue