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 conversation { get; set; }
|
||||||
public string content { get; set; }
|
public string content { get; set; }
|
||||||
//public Dictionary<string,string> contentMap { 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 string[] tag { get; set; }
|
||||||
//public Dictionary<string, string> replies;
|
//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
|
public interface IUserService
|
||||||
{
|
{
|
||||||
Actor GetUser(TwitterUser twitterUser);
|
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> 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);
|
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;
|
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)
|
public async Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity)
|
||||||
{
|
{
|
||||||
// Validate
|
// 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
|
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.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BirdsiteLive.DAL.Contracts;
|
using BirdsiteLive.DAL.Contracts;
|
||||||
|
using BirdsiteLive.DAL.Models;
|
||||||
using BirdsiteLive.Domain;
|
using BirdsiteLive.Domain;
|
||||||
using BirdsiteLive.Pipeline.Contracts;
|
using BirdsiteLive.Pipeline.Contracts;
|
||||||
using BirdsiteLive.Pipeline.Models;
|
using BirdsiteLive.Pipeline.Models;
|
||||||
using BirdsiteLive.Twitter;
|
using BirdsiteLive.Twitter;
|
||||||
|
using Tweetinvi.Models;
|
||||||
|
|
||||||
namespace BirdsiteLive.Pipeline.Processors
|
namespace BirdsiteLive.Pipeline.Processors
|
||||||
{
|
{
|
||||||
public class SendTweetsToFollowersProcessor : ISendTweetsToFollowersProcessor
|
public class SendTweetsToFollowersProcessor : ISendTweetsToFollowersProcessor
|
||||||
{
|
{
|
||||||
private readonly IActivityPubService _activityPubService;
|
private readonly IActivityPubService _activityPubService;
|
||||||
private readonly IUserService _userService;
|
private readonly IStatusService _statusService;
|
||||||
private readonly IFollowersDal _followersDal;
|
private readonly IFollowersDal _followersDal;
|
||||||
private readonly ITwitterUserDal _twitterUserDal;
|
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public SendTweetsToFollowersProcessor(IActivityPubService activityPubService, IUserService userService, IFollowersDal followersDal, ITwitterUserDal twitterUserDal)
|
public SendTweetsToFollowersProcessor(IActivityPubService activityPubService, IFollowersDal followersDal, IStatusService statusService)
|
||||||
{
|
{
|
||||||
_activityPubService = activityPubService;
|
_activityPubService = activityPubService;
|
||||||
_userService = userService;
|
|
||||||
_followersDal = followersDal;
|
_followersDal = followersDal;
|
||||||
_twitterUserDal = twitterUserDal;
|
_statusService = statusService;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public async Task ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct)
|
public async Task<UserWithTweetsToSync> ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var user = userWithTweetsToSync.User;
|
var user = userWithTweetsToSync.User;
|
||||||
var userId = user.Id;
|
var userId = user.Id;
|
||||||
|
|
||||||
foreach (var follower in userWithTweetsToSync.Followers)
|
foreach (var follower in userWithTweetsToSync.Followers)
|
||||||
{
|
{
|
||||||
var fromStatusId = follower.FollowingsSyncStatus[userId];
|
try
|
||||||
var tweetsToSend = userWithTweetsToSync.Tweets.Where(x => x.Id > fromStatusId).OrderBy(x => x.Id).ToList();
|
|
||||||
|
|
||||||
var syncStatus = fromStatusId;
|
|
||||||
foreach (var tweet in tweetsToSend)
|
|
||||||
{
|
{
|
||||||
var note = _userService.GetStatus(user.Acct, tweet);
|
await ProcessFollowerAsync(userWithTweetsToSync.Tweets, follower, userId, user);
|
||||||
var result = await _activityPubService.PostNewNoteActivity(note, user.Acct, tweet.Id.ToString(), follower.Host, follower.InboxUrl);
|
}
|
||||||
if (result == HttpStatusCode.Accepted)
|
catch (Exception e)
|
||||||
syncStatus = tweet.Id;
|
{
|
||||||
else
|
Console.WriteLine(e);
|
||||||
break;
|
//TODO handle error
|
||||||
}
|
}
|
||||||
|
|
||||||
follower.FollowingsSyncStatus[userId] = syncStatus;
|
|
||||||
await _followersDal.UpdateFollowerAsync(follower);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastPostedTweet = userWithTweetsToSync.Tweets.Select(x => x.Id).Max();
|
return userWithTweetsToSync;
|
||||||
var minimumSync = userWithTweetsToSync.Followers.Select(x => x.FollowingsSyncStatus[userId]).Min();
|
}
|
||||||
await _twitterUserDal.UpdateTwitterUserAsync(userId, lastPostedTweet, minimumSync);
|
|
||||||
|
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 },
|
//cc = new [] { apPublic },
|
||||||
sensitive = false,
|
sensitive = false,
|
||||||
content = "<p>Woooot</p>",
|
content = "<p>Woooot</p>",
|
||||||
attachment = new string[0],
|
attachment = new Attachment[0],
|
||||||
tag = new string[0]
|
tag = new string[0]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BirdsiteLive.ActivityPub;
|
using BirdsiteLive.ActivityPub;
|
||||||
|
@ -18,12 +19,14 @@ namespace BirdsiteLive.Controllers
|
||||||
{
|
{
|
||||||
private readonly ITwitterService _twitterService;
|
private readonly ITwitterService _twitterService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly IStatusService _statusService;
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public UsersController(ITwitterService twitterService, IUserService userService)
|
public UsersController(ITwitterService twitterService, IUserService userService, IStatusService statusService)
|
||||||
{
|
{
|
||||||
_twitterService = twitterService;
|
_twitterService = twitterService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_statusService = statusService;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -62,7 +65,7 @@ namespace BirdsiteLive.Controllers
|
||||||
//var user = _twitterService.GetUser(id);
|
//var user = _twitterService.GetUser(id);
|
||||||
//if (user == null) return NotFound();
|
//if (user == null) return NotFound();
|
||||||
|
|
||||||
var status = _userService.GetStatus(id, tweet);
|
var status = _statusService.GetStatus(id, tweet);
|
||||||
var jsonApUser = JsonConvert.SerializeObject(status);
|
var jsonApUser = JsonConvert.SerializeObject(status);
|
||||||
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
@ -78,6 +81,8 @@ namespace BirdsiteLive.Controllers
|
||||||
using (var reader = new StreamReader(Request.Body))
|
using (var reader = new StreamReader(Request.Body))
|
||||||
{
|
{
|
||||||
var body = await reader.ReadToEndAsync();
|
var body = await reader.ReadToEndAsync();
|
||||||
|
//System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body);
|
||||||
|
|
||||||
var activity = ApDeserializer.ProcessActivity(body);
|
var activity = ApDeserializer.ProcessActivity(body);
|
||||||
// Do something
|
// Do something
|
||||||
var signature = r.Headers["Signature"].First();
|
var signature = r.Headers["Signature"].First();
|
||||||
|
|
Loading…
Add table
Reference in a new issue