commit
62e0c3ee79
91 changed files with 4029 additions and 314 deletions
8
.github/workflows/dotnet-core.yml
vendored
8
.github/workflows/dotnet-core.yml
vendored
|
@ -1,10 +1,6 @@
|
|||
name: .NET Core
|
||||
name: ASP.NET Core Build & Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
@ -6,15 +6,12 @@ EXPOSE 80
|
|||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
|
||||
WORKDIR /src
|
||||
COPY ["BirdsiteLive/BirdsiteLive.csproj", "BirdsiteLive/"]
|
||||
RUN dotnet restore "BirdsiteLive/BirdsiteLive.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/BirdsiteLive"
|
||||
RUN dotnet build "BirdsiteLive.csproj" -c Release -o /app/build
|
||||
COPY ./src/ ./src/
|
||||
RUN dotnet restore "/src/BirdsiteLive/BirdsiteLive.csproj"
|
||||
RUN dotnet build "/src/BirdsiteLive/BirdsiteLive.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "BirdsiteLive.csproj" -c Release -o /app/publish
|
||||
RUN dotnet publish "/src/BirdsiteLive/BirdsiteLive.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
39
docker-compose.yml
Normal file
39
docker-compose.yml
Normal file
|
@ -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
|
|
@ -1,4 +1,5 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BirdsiteLive.ActivityPub
|
||||
{
|
||||
|
@ -6,41 +7,48 @@ namespace BirdsiteLive.ActivityPub
|
|||
{
|
||||
public static Activity ProcessActivity(string json)
|
||||
{
|
||||
var activity = JsonConvert.DeserializeObject<Activity>(json);
|
||||
switch (activity.type)
|
||||
try
|
||||
{
|
||||
case "Follow":
|
||||
return JsonConvert.DeserializeObject<ActivityFollow>(json);
|
||||
case "Undo":
|
||||
var a = JsonConvert.DeserializeObject<ActivityUndo>(json);
|
||||
if(a.apObject.type == "Follow")
|
||||
return JsonConvert.DeserializeObject<ActivityUndoFollow>(json);
|
||||
break;
|
||||
case "Accept":
|
||||
var accept = JsonConvert.DeserializeObject<ActivityAccept>(json);
|
||||
//var acceptType = JsonConvert.DeserializeObject<Activity>(accept.apObject);
|
||||
switch ((accept.apObject as dynamic).type.ToString())
|
||||
{
|
||||
case "Follow":
|
||||
var acceptFollow = new ActivityAcceptFollow()
|
||||
{
|
||||
type = accept.type,
|
||||
id = accept.id,
|
||||
actor = accept.actor,
|
||||
context = accept.context,
|
||||
apObject = new ActivityFollow()
|
||||
var activity = JsonConvert.DeserializeObject<Activity>(json);
|
||||
switch (activity.type)
|
||||
{
|
||||
case "Follow":
|
||||
return JsonConvert.DeserializeObject<ActivityFollow>(json);
|
||||
case "Undo":
|
||||
var a = JsonConvert.DeserializeObject<ActivityUndo>(json);
|
||||
if(a.apObject.type == "Follow")
|
||||
return JsonConvert.DeserializeObject<ActivityUndoFollow>(json);
|
||||
break;
|
||||
case "Accept":
|
||||
var accept = JsonConvert.DeserializeObject<ActivityAccept>(json);
|
||||
//var acceptType = JsonConvert.DeserializeObject<Activity>(accept.apObject);
|
||||
switch ((accept.apObject as dynamic).type.ToString())
|
||||
{
|
||||
case "Follow":
|
||||
var acceptFollow = new ActivityAcceptFollow()
|
||||
{
|
||||
id = (accept.apObject as dynamic).id?.ToString(),
|
||||
type = (accept.apObject as dynamic).type?.ToString(),
|
||||
actor = (accept.apObject as dynamic).actor?.ToString(),
|
||||
context = (accept.apObject as dynamic).context?.ToString(),
|
||||
apObject = (accept.apObject as dynamic).@object?.ToString()
|
||||
}
|
||||
};
|
||||
return acceptFollow;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
type = accept.type,
|
||||
id = accept.id,
|
||||
actor = accept.actor,
|
||||
context = accept.context,
|
||||
apObject = new ActivityFollow()
|
||||
{
|
||||
id = (accept.apObject as dynamic).id?.ToString(),
|
||||
type = (accept.apObject as dynamic).type?.ToString(),
|
||||
actor = (accept.apObject as dynamic).actor?.ToString(),
|
||||
context = (accept.apObject as dynamic).context?.ToString(),
|
||||
apObject = (accept.apObject as dynamic).@object?.ToString()
|
||||
}
|
||||
};
|
||||
return acceptFollow;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="System.Text.Json" Version="4.7.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace BirdsiteLive.ActivityPub
|
||||
{
|
||||
public class ActivityAcceptUndoFollow : Activity
|
||||
{
|
||||
[JsonProperty("object")]
|
||||
public ActivityUndoFollow apObject { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using BirdsiteLive.ActivityPub.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BirdsiteLive.ActivityPub
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using BirdsiteLive.ActivityPub.Converters;
|
||||
using System.Net;
|
||||
using BirdsiteLive.ActivityPub.Converters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BirdsiteLive.ActivityPub
|
||||
|
@ -11,13 +12,16 @@ namespace BirdsiteLive.ActivityPub
|
|||
public string[] context { get; set; } = new[] { "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" };
|
||||
public string id { get; set; }
|
||||
public string type { get; set; }
|
||||
public string followers { get; set; }
|
||||
public string preferredUsername { get; set; }
|
||||
public string name { get; set; }
|
||||
public string summary { get; set; }
|
||||
public string url { get; set; }
|
||||
public string inbox { get; set; }
|
||||
public bool? discoverable { get; set; } = true;
|
||||
public PublicKey publicKey { get; set; }
|
||||
public Image icon { get; set; }
|
||||
public Image image { get; set; }
|
||||
public EndPoints endpoints { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
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; }
|
||||
}
|
||||
}
|
7
src/BirdsiteLive.ActivityPub/Models/EndPoints.cs
Normal file
7
src/BirdsiteLive.ActivityPub/Models/EndPoints.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace BirdsiteLive.ActivityPub
|
||||
{
|
||||
public class EndPoints
|
||||
{
|
||||
public string sharedInbox { get; set; }
|
||||
}
|
||||
}
|
15
src/BirdsiteLive.ActivityPub/Models/Followers.cs
Normal file
15
src/BirdsiteLive.ActivityPub/Models/Followers.cs
Normal file
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BirdsiteLive.ActivityPub.Converters;
|
||||
using BirdsiteLive.ActivityPub.Converters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BirdsiteLive.ActivityPub
|
||||
namespace BirdsiteLive.ActivityPub.Models
|
||||
{
|
||||
public class Note
|
||||
{
|
||||
|
@ -24,8 +22,8 @@ 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 string[] tag { get; set; }
|
||||
public Attachment[] attachment { get; set; }
|
||||
public Tag[] tag { get; set; }
|
||||
//public Dictionary<string, string> replies;
|
||||
}
|
||||
}
|
8
src/BirdsiteLive.ActivityPub/Models/Tag.cs
Normal file
8
src/BirdsiteLive.ActivityPub/Models/Tag.cs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
11
src/BirdsiteLive.Common/Settings/DbSettings.cs
Normal file
11
src/BirdsiteLive.Common/Settings/DbSettings.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -3,5 +3,6 @@
|
|||
public class InstanceSettings
|
||||
{
|
||||
public string Domain { get; set; }
|
||||
public string AdminEmail { get; set; }
|
||||
}
|
||||
}
|
|
@ -4,7 +4,5 @@
|
|||
{
|
||||
public string ConsumerKey { get; set; }
|
||||
public string ConsumerSecret { get; set; }
|
||||
public string AccessToken { get; set; }
|
||||
public string AccessTokenSecret { get; set; }
|
||||
}
|
||||
}
|
7
src/BirdsiteLive.Common/Structs/DbTypes.cs
Normal file
7
src/BirdsiteLive.Common/Structs/DbTypes.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace BirdsiteLive.Common.Structs
|
||||
{
|
||||
public struct DbTypes
|
||||
{
|
||||
public static string Postgres = "postgres";
|
||||
}
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.ActivityPub;
|
||||
using BirdsiteLive.ActivityPub.Models;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using Newtonsoft.Json;
|
||||
using Org.BouncyCastle.Bcpg;
|
||||
|
||||
|
@ -13,16 +17,20 @@ namespace BirdsiteLive.Domain
|
|||
{
|
||||
Task<Actor> GetUser(string objectId);
|
||||
Task<HttpStatusCode> PostDataAsync<T>(T data, string targetHost, string actorUrl, string inbox = null);
|
||||
Task<HttpStatusCode> PostNewNoteActivity(Note note, string username, string noteId, string targetHost,
|
||||
string targetInbox);
|
||||
}
|
||||
|
||||
public class ActivityPubService : IActivityPubService
|
||||
{
|
||||
private readonly InstanceSettings _instanceSettings;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
#region Ctor
|
||||
public ActivityPubService(ICryptoService cryptoService)
|
||||
public ActivityPubService(ICryptoService cryptoService, InstanceSettings instanceSettings)
|
||||
{
|
||||
_cryptoService = cryptoService;
|
||||
_instanceSettings = instanceSettings;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
@ -37,6 +45,40 @@ namespace BirdsiteLive.Domain
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<HttpStatusCode> PostNewNoteActivity(Note note, string username, string noteId, string targetHost, string targetInbox)
|
||||
{
|
||||
//var username = "gra";
|
||||
var actor = $"https://{_instanceSettings.Domain}/users/{username}";
|
||||
//var targetHost = "mastodon.technology";
|
||||
//var target = $"{targetHost}/users/testtest";
|
||||
//var inbox = $"/users/testtest/inbox";
|
||||
|
||||
//var noteGuid = Guid.NewGuid();
|
||||
var noteUri = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{noteId}";
|
||||
|
||||
//var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteId}";
|
||||
//var to = $"{actor}/followers";
|
||||
//var apPublic = "https://www.w3.org/ns/activitystreams#Public";
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var nowString = now.ToString("s") + "Z";
|
||||
|
||||
var noteActivity = new ActivityCreateNote()
|
||||
{
|
||||
context = "https://www.w3.org/ns/activitystreams",
|
||||
id = $"{noteUri}/activity",
|
||||
type = "Create",
|
||||
actor = actor,
|
||||
published = nowString,
|
||||
|
||||
to = note.to,
|
||||
cc = note.cc,
|
||||
apObject = note
|
||||
};
|
||||
|
||||
return await PostDataAsync(noteActivity, targetHost, actor, targetInbox);
|
||||
}
|
||||
|
||||
public async Task<HttpStatusCode> PostDataAsync<T>(T data, string targetHost, string actorUrl, string inbox = null)
|
||||
{
|
||||
var usedInbox = $"/inbox";
|
||||
|
@ -47,20 +89,22 @@ namespace BirdsiteLive.Domain
|
|||
|
||||
var date = DateTime.UtcNow.ToUniversalTime();
|
||||
var httpDate = date.ToString("r");
|
||||
var signature = _cryptoService.SignAndGetSignatureHeader(date, actorUrl, targetHost, usedInbox);
|
||||
|
||||
|
||||
var digest = _cryptoService.ComputeSha256Hash(json);
|
||||
|
||||
var signature = _cryptoService.SignAndGetSignatureHeader(date, actorUrl, targetHost, digest, usedInbox);
|
||||
|
||||
var client = new HttpClient();
|
||||
var httpRequestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri($"https://{targetHost}/{usedInbox}"),
|
||||
RequestUri = new Uri($"https://{targetHost}{usedInbox}"),
|
||||
Headers =
|
||||
{
|
||||
{"Host", targetHost},
|
||||
{"Date", httpDate},
|
||||
{"Signature", signature}
|
||||
{"Signature", signature},
|
||||
{"Digest", $"SHA-256={digest}"}
|
||||
},
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/ld+json")
|
||||
};
|
||||
|
@ -68,5 +112,7 @@ namespace BirdsiteLive.Domain
|
|||
var response = await client.SendAsync(httpRequestMessage);
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
<ProjectReference Include="..\BirdsiteLive.ActivityPub\BirdsiteLive.ActivityPub.csproj" />
|
||||
<ProjectReference Include="..\BirdsiteLive.Cryptography\BirdsiteLive.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\BirdsiteLive.Twitter\BirdsiteLive.Twitter.csproj" />
|
||||
<ProjectReference Include="..\DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using BirdsiteLive.Domain.Factories;
|
||||
|
||||
|
@ -7,7 +8,8 @@ namespace BirdsiteLive.Domain
|
|||
public interface ICryptoService
|
||||
{
|
||||
string GetUserPem(string id);
|
||||
string SignAndGetSignatureHeader(DateTime date, string actor, string host, string inbox = null);
|
||||
string SignAndGetSignatureHeader(DateTime date, string actor, string host, string digest, string inbox);
|
||||
string ComputeSha256Hash(string data);
|
||||
}
|
||||
|
||||
public class CryptoService : ICryptoService
|
||||
|
@ -33,7 +35,7 @@ namespace BirdsiteLive.Domain
|
|||
/// <param name="actor">in the form of https://domain.io/actor</param>
|
||||
/// <param name="host">in the form of domain.io</param>
|
||||
/// <returns></returns>
|
||||
public string SignAndGetSignatureHeader(DateTime date, string actor, string targethost, string inbox = null)
|
||||
public string SignAndGetSignatureHeader(DateTime date, string actor, string targethost, string digest, string inbox)
|
||||
{
|
||||
var usedInbox = "/inbox";
|
||||
if (!string.IsNullOrWhiteSpace(inbox))
|
||||
|
@ -41,13 +43,24 @@ namespace BirdsiteLive.Domain
|
|||
|
||||
var httpDate = date.ToString("r");
|
||||
|
||||
var signedString = $"(request-target): post {usedInbox}\nhost: {targethost}\ndate: {httpDate}";
|
||||
var signedString = $"(request-target): post {usedInbox}\nhost: {targethost}\ndate: {httpDate}\ndigest: SHA-256={digest}";
|
||||
var signedStringBytes = Encoding.UTF8.GetBytes(signedString);
|
||||
var signature = _magicKeyFactory.GetMagicKey().Sign(signedStringBytes);
|
||||
var sig64 = Convert.ToBase64String(signature);
|
||||
|
||||
var header = "keyId=\"" + actor + "\",headers=\"(request-target) host date\",signature=\"" + sig64 + "\"";
|
||||
var header = "keyId=\"" + actor + "\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"" + sig64 + "\"";
|
||||
return header;
|
||||
}
|
||||
|
||||
public string ComputeSha256Hash(string data)
|
||||
{
|
||||
// Create a SHA256
|
||||
using (SHA256 sha256Hash = SHA256.Create())
|
||||
{
|
||||
// ComputeHash - returns byte array
|
||||
byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
src/BirdsiteLive.Domain/StatusService.cs
Normal file
90
src/BirdsiteLive.Domain/StatusService.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
130
src/BirdsiteLive.Domain/Tools/StatusExtractor.cs
Normal file
130
src/BirdsiteLive.Domain/Tools/StatusExtractor.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using BirdsiteLive.ActivityPub;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Cryptography;
|
||||
using BirdsiteLive.Domain.BusinessUseCases;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Tweetinvi.Core.Exceptions;
|
||||
using Tweetinvi.Models;
|
||||
|
@ -17,22 +18,28 @@ namespace BirdsiteLive.Domain
|
|||
public interface IUserService
|
||||
{
|
||||
Actor GetUser(TwitterUser twitterUser);
|
||||
Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity);
|
||||
Note GetStatus(TwitterUser user, ITweet tweet);
|
||||
Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity, string body);
|
||||
Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityUndoFollow activity, string body);
|
||||
}
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly IProcessFollowUser _processFollowUser;
|
||||
private readonly IProcessUndoFollowUser _processUndoFollowUser;
|
||||
|
||||
private readonly InstanceSettings _instanceSettings;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IActivityPubService _activityPubService;
|
||||
private readonly string _host;
|
||||
|
||||
#region Ctor
|
||||
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService)
|
||||
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser)
|
||||
{
|
||||
_instanceSettings = instanceSettings;
|
||||
_cryptoService = cryptoService;
|
||||
_activityPubService = activityPubService;
|
||||
_host = $"https://{instanceSettings.Domain.Replace("https://",string.Empty).Replace("http://", string.Empty).TrimEnd('/')}";
|
||||
_processFollowUser = processFollowUser;
|
||||
_processUndoFollowUser = processUndoFollowUser;
|
||||
//_host = $"https://{instanceSettings.Domain.Replace("https://",string.Empty).Replace("http://", string.Empty).TrimEnd('/')}";
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
@ -40,17 +47,18 @@ namespace BirdsiteLive.Domain
|
|||
{
|
||||
var user = new Actor
|
||||
{
|
||||
id = $"{_host}/users/{twitterUser.Acct}",
|
||||
type = "Person",
|
||||
id = $"https://{_instanceSettings.Domain}/users/{twitterUser.Acct}",
|
||||
type = "Service", //Person Service
|
||||
followers = $"https://{_instanceSettings.Domain}/users/{twitterUser.Acct}/followers",
|
||||
preferredUsername = twitterUser.Acct,
|
||||
name = twitterUser.Name,
|
||||
inbox = $"{_host}/users/{twitterUser.Acct}/inbox",
|
||||
inbox = $"https://{_instanceSettings.Domain}/users/{twitterUser.Acct}/inbox",
|
||||
summary = twitterUser.Description,
|
||||
url = $"{_host}/@{twitterUser.Acct}",
|
||||
url = $"https://{_instanceSettings.Domain}/@{twitterUser.Acct}",
|
||||
publicKey = new PublicKey()
|
||||
{
|
||||
id = $"{_host}/users/{twitterUser.Acct}#main-key",
|
||||
owner = $"{_host}/users/{twitterUser.Acct}",
|
||||
id = $"https://{_instanceSettings.Domain}/users/{twitterUser.Acct}#main-key",
|
||||
owner = $"https://{_instanceSettings.Domain}/users/{twitterUser.Acct}",
|
||||
publicKeyPem = _cryptoService.GetUserPem(twitterUser.Acct)
|
||||
},
|
||||
icon = new Image
|
||||
|
@ -62,53 +70,36 @@ namespace BirdsiteLive.Domain
|
|||
{
|
||||
mediaType = "image/jpeg",
|
||||
url = twitterUser.ProfileBannerURL
|
||||
},
|
||||
endpoints = new EndPoints
|
||||
{
|
||||
sharedInbox = $"https://{_instanceSettings.Domain}/inbox"
|
||||
}
|
||||
};
|
||||
return user;
|
||||
}
|
||||
|
||||
public Note GetStatus(TwitterUser user, ITweet tweet)
|
||||
{
|
||||
var actor = GetUser(user);
|
||||
|
||||
var actorUrl = $"{_host}/users/{user.Acct}";
|
||||
var noteId = $"{_host}/users/{user.Acct}/statuses/{tweet.Id}";
|
||||
var noteUrl = $"{_host}/@{user.Acct}/{tweet.Id}";
|
||||
|
||||
var to = $"{actor}/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, string body)
|
||||
{
|
||||
// Validate
|
||||
if (!await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders)) return false;
|
||||
var sigValidation = await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders, body);
|
||||
if (!sigValidation.SignatureIsValidated) return false;
|
||||
|
||||
// Save Follow in DB
|
||||
var followerUserName = sigValidation.User.preferredUsername.ToLowerInvariant();
|
||||
var followerHost = sigValidation.User.url.Replace("https://", string.Empty).Split('/').First();
|
||||
var followerInbox = sigValidation.User.inbox;
|
||||
var followerSharedInbox = sigValidation.User?.endpoints?.sharedInbox;
|
||||
var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty);
|
||||
|
||||
// Make sure to only keep routes
|
||||
followerInbox = OnlyKeepRoute(followerInbox, followerHost);
|
||||
followerSharedInbox = OnlyKeepRoute(followerSharedInbox, followerHost);
|
||||
|
||||
// Send Accept Activity
|
||||
var targetHost = activity.actor.Replace("https://", string.Empty).Split('/').First();
|
||||
// Execute
|
||||
await _processFollowUser.ExecuteAsync(followerUserName, followerHost, twitterUser, followerInbox, followerSharedInbox);
|
||||
|
||||
// Send Accept Activity
|
||||
var acceptFollow = new ActivityAcceptFollow()
|
||||
{
|
||||
context = "https://www.w3.org/ns/activitystreams",
|
||||
|
@ -123,12 +114,70 @@ namespace BirdsiteLive.Domain
|
|||
apObject = activity.apObject
|
||||
}
|
||||
};
|
||||
var result = await _activityPubService.PostDataAsync(acceptFollow, targetHost, activity.apObject);
|
||||
var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject);
|
||||
return result == HttpStatusCode.Accepted;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders)
|
||||
|
||||
private string OnlyKeepRoute(string inbox, string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(inbox))
|
||||
return null;
|
||||
|
||||
if (inbox.Contains(host))
|
||||
inbox = inbox.Split(new[] { host }, StringSplitOptions.RemoveEmptyEntries).Last();
|
||||
|
||||
return inbox;
|
||||
}
|
||||
|
||||
public async Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString,
|
||||
Dictionary<string, string> requestHeaders, ActivityUndoFollow activity, string body)
|
||||
{
|
||||
// Validate
|
||||
var sigValidation = await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders, body);
|
||||
if (!sigValidation.SignatureIsValidated) return false;
|
||||
|
||||
// Save Follow in DB
|
||||
var followerUserName = sigValidation.User.name.ToLowerInvariant();
|
||||
var followerHost = sigValidation.User.url.Replace("https://", string.Empty).Split('/').First();
|
||||
//var followerInbox = sigValidation.User.inbox;
|
||||
var twitterUser = activity.apObject.apObject.Split('/').Last().Replace("@", string.Empty);
|
||||
await _processUndoFollowUser.ExecuteAsync(followerUserName, followerHost, twitterUser);
|
||||
|
||||
// Send Accept Activity
|
||||
var acceptFollow = new ActivityAcceptUndoFollow()
|
||||
{
|
||||
context = "https://www.w3.org/ns/activitystreams",
|
||||
id = $"{activity.apObject.apObject}#accepts/undofollows/{Guid.NewGuid()}",
|
||||
type = "Accept",
|
||||
actor = activity.apObject.apObject,
|
||||
apObject = new ActivityUndoFollow()
|
||||
{
|
||||
id = activity.id,
|
||||
type = activity.type,
|
||||
actor = activity.actor,
|
||||
apObject = activity.apObject
|
||||
}
|
||||
};
|
||||
var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject.apObject);
|
||||
return result == HttpStatusCode.Accepted;
|
||||
}
|
||||
|
||||
private async Task<SignatureValidationResult> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders, string body)
|
||||
{
|
||||
//Check Date Validity
|
||||
var date = requestHeaders["date"];
|
||||
var d = DateTime.Parse(date).ToUniversalTime();
|
||||
var now = DateTime.UtcNow;
|
||||
var delta = Math.Abs((d - now).TotalSeconds);
|
||||
if (delta > 30) return new SignatureValidationResult { SignatureIsValidated = false };
|
||||
|
||||
//Check Digest
|
||||
var digest = requestHeaders["digest"];
|
||||
var digestHash = digest.Split(new [] {"SHA-256="},StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
|
||||
var calculatedDigestHash = _cryptoService.ComputeSha256Hash(body);
|
||||
if (digestHash != calculatedDigestHash) return new SignatureValidationResult { SignatureIsValidated = false };
|
||||
|
||||
//Check Signature
|
||||
var signatures = rawSig.Split(',');
|
||||
var signature_header = new Dictionary<string, string>();
|
||||
foreach (var signature in signatures)
|
||||
|
@ -184,7 +233,17 @@ namespace BirdsiteLive.Domain
|
|||
|
||||
var result = signKey.VerifyData(Encoding.UTF8.GetBytes(toSign.ToString()), sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
return result;
|
||||
return new SignatureValidationResult()
|
||||
{
|
||||
SignatureIsValidated = result,
|
||||
User = remoteUser
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class SignatureValidationResult
|
||||
{
|
||||
public bool SignatureIsValidated { get; set; }
|
||||
public Actor User { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
19
src/BirdsiteLive.Pipeline/BirdsiteLive.Pipeline.csproj
Normal file
19
src/BirdsiteLive.Pipeline/BirdsiteLive.Pipeline.csproj
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
13
src/BirdsiteLive.Pipeline/Models/UserWithTweetsToSync.cs
Normal file
13
src/BirdsiteLive.Pipeline/Models/UserWithTweetsToSync.cs
Normal file
|
@ -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)
|
||||
{
|
||||
var userId = user.Id;
|
||||
var inbox = followersPerInstance.First().SharedInboxRoute;
|
||||
|
||||
var fromStatusId = followersPerInstance
|
||||
.Max(x => x.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(), host, inbox);
|
||||
|
||||
if (result == HttpStatusCode.Accepted)
|
||||
syncStatus = tweet.Id;
|
||||
else
|
||||
throw new Exception("Posting new note activity failed");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (syncStatus != fromStatusId)
|
||||
{
|
||||
foreach (var f in followersPerInstance)
|
||||
{
|
||||
f.FollowingsSyncStatus[userId] = syncStatus;
|
||||
await _followersDal.UpdateFollowerAsync(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
src/BirdsiteLive.Pipeline/StatusPublicationPipeline.cs
Normal file
62
src/BirdsiteLive.Pipeline/StatusPublicationPipeline.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks.Dataflow;
|
||||
using BirdsiteLive.DAL.Models;
|
||||
using BirdsiteLive.Pipeline.Contracts;
|
||||
using BirdsiteLive.Pipeline.Models;
|
||||
|
||||
namespace BirdsiteLive.Pipeline
|
||||
{
|
||||
public interface IStatusPublicationPipeline
|
||||
{
|
||||
Task ExecuteAsync(CancellationToken ct);
|
||||
}
|
||||
|
||||
public class StatusPublicationPipeline : IStatusPublicationPipeline
|
||||
{
|
||||
private readonly IRetrieveTwitterUsersProcessor _retrieveTwitterAccountsProcessor;
|
||||
private readonly IRetrieveTweetsProcessor _retrieveTweetsProcessor;
|
||||
private readonly IRetrieveFollowersProcessor _retrieveFollowersProcessor;
|
||||
private readonly ISendTweetsToFollowersProcessor _sendTweetsToFollowersProcessor;
|
||||
|
||||
#region Ctor
|
||||
public StatusPublicationPipeline(IRetrieveTweetsProcessor retrieveTweetsProcessor, IRetrieveTwitterUsersProcessor retrieveTwitterAccountsProcessor, IRetrieveFollowersProcessor retrieveFollowersProcessor, ISendTweetsToFollowersProcessor sendTweetsToFollowersProcessor)
|
||||
{
|
||||
_retrieveTweetsProcessor = retrieveTweetsProcessor;
|
||||
_retrieveTwitterAccountsProcessor = retrieveTwitterAccountsProcessor;
|
||||
_retrieveFollowersProcessor = retrieveFollowersProcessor;
|
||||
_sendTweetsToFollowersProcessor = sendTweetsToFollowersProcessor;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
// Create blocks
|
||||
var twitterUsersBufferBlock = new BufferBlock<SyncTwitterUser[]>(new DataflowBlockOptions { BoundedCapacity = 1, CancellationToken = ct});
|
||||
var retrieveTweetsBlock = new TransformBlock<SyncTwitterUser[], UserWithTweetsToSync[]>(async x => await _retrieveTweetsProcessor.ProcessAsync(x, ct));
|
||||
var retrieveTweetsBufferBlock = new BufferBlock<UserWithTweetsToSync[]>(new DataflowBlockOptions { BoundedCapacity = 1, CancellationToken = ct });
|
||||
var retrieveFollowersBlock = new TransformManyBlock<UserWithTweetsToSync[], UserWithTweetsToSync>(async x => await _retrieveFollowersProcessor.ProcessAsync(x, ct));
|
||||
var retrieveFollowersBufferBlock = new BufferBlock<UserWithTweetsToSync>(new DataflowBlockOptions { BoundedCapacity = 20, CancellationToken = ct });
|
||||
var sendTweetsToFollowersBlock = new ActionBlock<UserWithTweetsToSync>(async x => await _sendTweetsToFollowersProcessor.ProcessAsync(x, ct), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 5, CancellationToken = ct});
|
||||
|
||||
// Link pipeline
|
||||
twitterUsersBufferBlock.LinkTo(retrieveTweetsBlock, new DataflowLinkOptions {PropagateCompletion = true});
|
||||
retrieveTweetsBlock.LinkTo(retrieveTweetsBufferBlock, new DataflowLinkOptions { PropagateCompletion = true });
|
||||
retrieveTweetsBufferBlock.LinkTo(retrieveFollowersBlock, new DataflowLinkOptions { PropagateCompletion = true });
|
||||
retrieveFollowersBlock.LinkTo(retrieveFollowersBufferBlock, new DataflowLinkOptions { PropagateCompletion = true });
|
||||
retrieveFollowersBufferBlock.LinkTo(sendTweetsToFollowersBlock, new DataflowLinkOptions { PropagateCompletion = true });
|
||||
|
||||
// Launch twitter user retriever
|
||||
var retrieveTwitterAccountsTask = _retrieveTwitterAccountsProcessor.GetTwitterUsersAsync(twitterUsersBufferBlock, ct);
|
||||
|
||||
// Wait
|
||||
await Task.WhenAny(new []{ retrieveTwitterAccountsTask , sendTweetsToFollowersBlock.Completion});
|
||||
|
||||
var foreground = Console.ForegroundColor;
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("An error occured, pipeline stopped");
|
||||
Console.ForegroundColor = foreground;
|
||||
}
|
||||
}
|
||||
}
|
108
src/BirdsiteLive.Twitter/Extractors/TweetExtractor.cs
Normal file
108
src/BirdsiteLive.Twitter/Extractors/TweetExtractor.cs
Normal file
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Tweetinvi.Models;
|
||||
using Tweetinvi.Models.Entities;
|
||||
|
||||
namespace BirdsiteLive.Twitter.Extractors
|
||||
{
|
||||
public interface ITweetExtractor
|
||||
{
|
||||
ExtractedTweet Extract(ITweet tweet);
|
||||
}
|
||||
|
||||
public class TweetExtractor : ITweetExtractor
|
||||
{
|
||||
public ExtractedTweet Extract(ITweet tweet)
|
||||
{
|
||||
var extractedTweet = new ExtractedTweet
|
||||
{
|
||||
Id = tweet.Id,
|
||||
InReplyToStatusId = tweet.InReplyToStatusId,
|
||||
InReplyToAccount = tweet.InReplyToScreenName,
|
||||
MessageContent = ExtractMessage(tweet),
|
||||
Media = ExtractMedia(tweet.Media),
|
||||
CreatedAt = tweet.CreatedAt.ToUniversalTime()
|
||||
};
|
||||
return extractedTweet;
|
||||
}
|
||||
|
||||
public string ExtractMessage(ITweet tweet)
|
||||
{
|
||||
var tweetUrls = tweet.Media.Select(x => x.URL).Distinct();
|
||||
var message = tweet.FullText;
|
||||
foreach (var tweetUrl in tweetUrls)
|
||||
message = message.Replace(tweetUrl, string.Empty).Trim();
|
||||
|
||||
if (tweet.QuotedTweet != null) message = $"[Quote RT]{Environment.NewLine}{message}";
|
||||
if (tweet.IsRetweet)
|
||||
{
|
||||
if (tweet.RetweetedTweet != null)
|
||||
message = $"[RT @{tweet.RetweetedTweet.CreatedBy.ScreenName}]{Environment.NewLine}{tweet.RetweetedTweet.FullText}";
|
||||
else
|
||||
message = message.Replace("RT", "[RT]");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public ExtractedMedia[] ExtractMedia(List<IMediaEntity> media)
|
||||
{
|
||||
var result = new List<ExtractedMedia>();
|
||||
|
||||
foreach (var m in media)
|
||||
{
|
||||
var mediaUrl = GetMediaUrl(m);
|
||||
var mediaType = GetMediaType(m.MediaType, mediaUrl);
|
||||
if (mediaType == null) continue;
|
||||
|
||||
var att = new ExtractedMedia
|
||||
{
|
||||
MediaType = mediaType,
|
||||
Url = mediaUrl
|
||||
};
|
||||
result.Add(att);
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
}
|
8
src/BirdsiteLive.Twitter/Models/ExtractedMedia.cs
Normal file
8
src/BirdsiteLive.Twitter/Models/ExtractedMedia.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace BirdsiteLive.Twitter.Models
|
||||
{
|
||||
public class ExtractedMedia
|
||||
{
|
||||
public string MediaType { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
15
src/BirdsiteLive.Twitter/Models/ExtractedTweet.cs
Normal file
15
src/BirdsiteLive.Twitter/Models/ExtractedTweet.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace BirdsiteLive.Twitter.Models
|
||||
{
|
||||
public class ExtractedTweet
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long? InReplyToStatusId { get; set; }
|
||||
public string MessageContent { get; set; }
|
||||
public ExtractedMedia[] Media { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string InReplyToAccount { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,33 +1,41 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Twitter.Extractors;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Tweetinvi;
|
||||
using Tweetinvi.Models;
|
||||
using Tweetinvi.Models.Entities;
|
||||
using Tweetinvi.Parameters;
|
||||
|
||||
namespace BirdsiteLive.Twitter
|
||||
{
|
||||
public interface ITwitterService
|
||||
{
|
||||
TwitterUser GetUser(string username);
|
||||
ITweet GetTweet(long statusId);
|
||||
ExtractedTweet GetTweet(long statusId);
|
||||
ExtractedTweet[] GetTimeline(string username, int nberTweets, long fromTweetId = -1);
|
||||
}
|
||||
|
||||
public class TwitterService : ITwitterService
|
||||
{
|
||||
private readonly TwitterSettings _settings;
|
||||
private readonly ITweetExtractor _tweetExtractor;
|
||||
|
||||
#region Ctor
|
||||
public TwitterService(TwitterSettings settings)
|
||||
public TwitterService(TwitterSettings settings, ITweetExtractor tweetExtractor)
|
||||
{
|
||||
_settings = settings;
|
||||
_tweetExtractor = tweetExtractor;
|
||||
Auth.SetApplicationOnlyCredentials(_settings.ConsumerKey, _settings.ConsumerSecret, true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public TwitterUser GetUser(string username)
|
||||
{
|
||||
//Auth.SetUserCredentials(_settings.ConsumerKey, _settings.ConsumerSecret, _settings.AccessToken, _settings.AccessTokenSecret);
|
||||
Auth.SetApplicationOnlyCredentials(_settings.ConsumerKey, _settings.ConsumerSecret, true);
|
||||
var user = User.GetUserFromScreenName(username);
|
||||
if (user == null) return null;
|
||||
|
||||
|
@ -43,11 +51,37 @@ namespace BirdsiteLive.Twitter
|
|||
};
|
||||
}
|
||||
|
||||
public ITweet GetTweet(long statusId)
|
||||
public ExtractedTweet GetTweet(long statusId)
|
||||
{
|
||||
Auth.SetApplicationOnlyCredentials(_settings.ConsumerKey, _settings.ConsumerSecret, true);
|
||||
TweetinviConfig.CurrentThreadSettings.TweetMode = TweetMode.Extended;
|
||||
var tweet = Tweet.GetTweet(statusId);
|
||||
return tweet;
|
||||
return _tweetExtractor.Extract(tweet);
|
||||
}
|
||||
|
||||
public ExtractedTweet[] GetTimeline(string username, int nberTweets, long fromTweetId = -1)
|
||||
{
|
||||
TweetinviConfig.CurrentThreadSettings.TweetMode = TweetMode.Extended;
|
||||
|
||||
var user = User.GetUserFromScreenName(username);
|
||||
var tweets = new List<ITweet>();
|
||||
if (fromTweetId == -1)
|
||||
{
|
||||
var timeline = Timeline.GetUserTimeline(user.Id, nberTweets);
|
||||
if (timeline != null) tweets.AddRange(timeline);
|
||||
}
|
||||
else
|
||||
{
|
||||
var timelineRequestParameters = new UserTimelineParameters
|
||||
{
|
||||
SinceId = fromTweetId,
|
||||
MaximumNumberOfTweetsToRetrieve = nberTweets
|
||||
};
|
||||
var timeline = Timeline.GetUserTimeline(user.Id, timelineRequestParameters);
|
||||
if (timeline != null) tweets.AddRange(timeline);
|
||||
}
|
||||
|
||||
return tweets.Select(_tweetExtractor.Extract).ToArray();
|
||||
//return tweets.Where(x => returnReplies || string.IsNullOrWhiteSpace(x.InReplyToScreenName)).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,11 +25,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.ActivityPub.Te
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataAccessLayers", "DataAccessLayers", "{CFAB3509-3931-42DB-AC97-4F91FC2D849C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.DAL", "DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj", "{47058CAB-DC43-4DD1-8F68-D3D625332905}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.DAL", "DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj", "{47058CAB-DC43-4DD1-8F68-D3D625332905}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.DAL.Postgres", "DataAccessLayers\BirdsiteLive.DAL.Postgres\BirdsiteLive.DAL.Postgres.csproj", "{87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.DAL.Postgres", "DataAccessLayers\BirdsiteLive.DAL.Postgres\BirdsiteLive.DAL.Postgres.csproj", "{87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.DAL.Postgres.Tests", "Tests\BirdsiteLive.DAL.Postgres.Tests\BirdsiteLive.DAL.Postgres.Tests.csproj", "{CD9489BF-69C8-4705-8774-81C45F4F8FE1}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.DAL.Postgres.Tests", "Tests\BirdsiteLive.DAL.Postgres.Tests\BirdsiteLive.DAL.Postgres.Tests.csproj", "{CD9489BF-69C8-4705-8774-81C45F4F8FE1}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pipeline", "Pipeline", "{DA3C160C-4811-4E26-A5AD-42B81FAF2D7C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Pipeline", "BirdsiteLive.Pipeline\BirdsiteLive.Pipeline.csproj", "{2A8CC30D-D775-47D1-9388-F72A5C32DE2A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Domain.Tests", "Tests\BirdsiteLive.Domain.Tests\BirdsiteLive.Domain.Tests.csproj", "{F544D745-89A8-4DEA-B61C-A7E6C53C1D63}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Pipeline.Tests", "Tests\BirdsiteLive.Pipeline.Tests\BirdsiteLive.Pipeline.Tests.csproj", "{BF51CA81-5A7A-46F8-B4FB-861C6BE59298}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
@ -81,6 +89,18 @@ Global
|
|||
{CD9489BF-69C8-4705-8774-81C45F4F8FE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CD9489BF-69C8-4705-8774-81C45F4F8FE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CD9489BF-69C8-4705-8774-81C45F4F8FE1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2A8CC30D-D775-47D1-9388-F72A5C32DE2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2A8CC30D-D775-47D1-9388-F72A5C32DE2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2A8CC30D-D775-47D1-9388-F72A5C32DE2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2A8CC30D-D775-47D1-9388-F72A5C32DE2A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F544D745-89A8-4DEA-B61C-A7E6C53C1D63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F544D745-89A8-4DEA-B61C-A7E6C53C1D63}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F544D745-89A8-4DEA-B61C-A7E6C53C1D63}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F544D745-89A8-4DEA-B61C-A7E6C53C1D63}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BF51CA81-5A7A-46F8-B4FB-861C6BE59298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF51CA81-5A7A-46F8-B4FB-861C6BE59298}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF51CA81-5A7A-46F8-B4FB-861C6BE59298}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BF51CA81-5A7A-46F8-B4FB-861C6BE59298}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -96,6 +116,9 @@ Global
|
|||
{47058CAB-DC43-4DD1-8F68-D3D625332905} = {CFAB3509-3931-42DB-AC97-4F91FC2D849C}
|
||||
{87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6} = {CFAB3509-3931-42DB-AC97-4F91FC2D849C}
|
||||
{CD9489BF-69C8-4705-8774-81C45F4F8FE1} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
|
||||
{2A8CC30D-D775-47D1-9388-F72A5C32DE2A} = {DA3C160C-4811-4E26-A5AD-42B81FAF2D7C}
|
||||
{F544D745-89A8-4DEA-B61C-A7E6C53C1D63} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
|
||||
{BF51CA81-5A7A-46F8-B4FB-861C6BE59298} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {69E8DCAD-4C37-4010-858F-5F94E6FBABCE}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<Version>0.1.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -16,7 +17,9 @@
|
|||
<ProjectReference Include="..\BirdsiteLive.Common\BirdsiteLive.Common.csproj" />
|
||||
<ProjectReference Include="..\BirdsiteLive.Cryptography\BirdsiteLive.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\BirdsiteLive.Domain\BirdsiteLive.Domain.csproj" />
|
||||
<ProjectReference Include="..\BirdsiteLive.Pipeline\BirdsiteLive.Pipeline.csproj" />
|
||||
<ProjectReference Include="..\BirdsiteLive.Twitter\BirdsiteLive.Twitter.csproj" />
|
||||
<ProjectReference Include="..\DataAccessLayers\BirdsiteLive.DAL.Postgres\BirdsiteLive.DAL.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.ActivityPub;
|
||||
using BirdsiteLive.ActivityPub.Models;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Domain;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -92,8 +93,8 @@ namespace BirdsiteLive.Controllers
|
|||
//cc = new [] { apPublic },
|
||||
sensitive = false,
|
||||
content = "<p>Woooot</p>",
|
||||
attachment = new string[0],
|
||||
tag = new string[0]
|
||||
attachment = new Attachment[0],
|
||||
tag = new Tag[0]
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -33,5 +33,11 @@ namespace BirdsiteLive.Controllers
|
|||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult Index(string handle)
|
||||
{
|
||||
return RedirectToAction("Index", "Users", new {id = handle});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,11 @@ namespace BirdsiteLive.Controllers
|
|||
using (var reader = new StreamReader(Request.Body))
|
||||
{
|
||||
var body = await reader.ReadToEndAsync();
|
||||
|
||||
//System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body);
|
||||
|
||||
}
|
||||
|
||||
|
||||
throw new NotImplementedException();
|
||||
return Accepted();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,15 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.ActivityPub;
|
||||
using BirdsiteLive.ActivityPub.Models;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Domain;
|
||||
using BirdsiteLive.Models;
|
||||
using BirdsiteLive.Twitter;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -18,12 +23,16 @@ namespace BirdsiteLive.Controllers
|
|||
{
|
||||
private readonly ITwitterService _twitterService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IStatusService _statusService;
|
||||
private readonly InstanceSettings _instanceSettings;
|
||||
|
||||
#region Ctor
|
||||
public UsersController(ITwitterService twitterService, IUserService userService)
|
||||
public UsersController(ITwitterService twitterService, IUserService userService, IStatusService statusService, InstanceSettings instanceSettings)
|
||||
{
|
||||
_twitterService = twitterService;
|
||||
_userService = userService;
|
||||
_statusService = statusService;
|
||||
_instanceSettings = instanceSettings;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
@ -42,7 +51,17 @@ namespace BirdsiteLive.Controllers
|
|||
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
||||
}
|
||||
|
||||
return View(user);
|
||||
var displayableUser = new DisplayTwitterUser
|
||||
{
|
||||
Name = user.Name,
|
||||
Description = user.Description,
|
||||
Acct = user.Acct,
|
||||
Url = user.Url,
|
||||
ProfileImageUrl = user.ProfileImageUrl,
|
||||
|
||||
InstanceHandle = $"@{user.Acct}@{_instanceSettings.Domain}"
|
||||
};
|
||||
return View(displayableUser);
|
||||
}
|
||||
|
||||
[Route("/@{id}/{statusId}")]
|
||||
|
@ -54,15 +73,15 @@ namespace BirdsiteLive.Controllers
|
|||
{
|
||||
if (!long.TryParse(statusId, out var parsedStatusId))
|
||||
return NotFound();
|
||||
|
||||
|
||||
var tweet = _twitterService.GetTweet(parsedStatusId);
|
||||
if(tweet == null)
|
||||
if (tweet == null)
|
||||
return NotFound();
|
||||
|
||||
var user = _twitterService.GetUser(id);
|
||||
if (user == null) return NotFound();
|
||||
//var user = _twitterService.GetUser(id);
|
||||
//if (user == null) return NotFound();
|
||||
|
||||
var status = _userService.GetStatus(user, tweet);
|
||||
var status = _statusService.GetStatus(id, tweet);
|
||||
var jsonApUser = JsonConvert.SerializeObject(status);
|
||||
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
||||
}
|
||||
|
@ -78,24 +97,54 @@ 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();
|
||||
|
||||
switch (activity.type)
|
||||
Console.WriteLine(body);
|
||||
Console.WriteLine();
|
||||
|
||||
switch (activity?.type)
|
||||
{
|
||||
case "Follow":
|
||||
var succeeded = await _userService.FollowRequestedAsync(r.Headers["Signature"].First(), r.Method, r.Path, r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow);
|
||||
if (succeeded) return Accepted();
|
||||
else return Unauthorized();
|
||||
break;
|
||||
{
|
||||
var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path,
|
||||
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow, body);
|
||||
if (succeeded) return Accepted();
|
||||
else return Unauthorized();
|
||||
}
|
||||
case "Undo":
|
||||
if (activity is ActivityUndoFollow)
|
||||
{
|
||||
var succeeded = await _userService.UndoFollowRequestedAsync(signature, r.Method, r.Path,
|
||||
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityUndoFollow, body);
|
||||
if (succeeded) return Accepted();
|
||||
else return Unauthorized();
|
||||
}
|
||||
return Accepted();
|
||||
default:
|
||||
return Accepted();
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
[Route("/users/{id}/followers")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Followers(string id)
|
||||
{
|
||||
var r = Request.Headers["Accept"].First();
|
||||
if (!r.Contains("application/activity+json")) return NotFound();
|
||||
|
||||
var followers = new Followers
|
||||
{
|
||||
id = $"https://{_instanceSettings.Domain}/users/{id}/followers"
|
||||
};
|
||||
var jsonApUser = JsonConvert.SerializeObject(followers);
|
||||
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
||||
}
|
||||
|
||||
private Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Models;
|
||||
using BirdsiteLive.Models.WellKnownModels;
|
||||
using BirdsiteLive.Twitter;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -17,10 +18,10 @@ namespace BirdsiteLive.Controllers
|
|||
private readonly InstanceSettings _settings;
|
||||
|
||||
#region Ctor
|
||||
public WellKnownController(IOptions<InstanceSettings> settings, ITwitterService twitterService)
|
||||
public WellKnownController(InstanceSettings settings, ITwitterService twitterService)
|
||||
{
|
||||
_twitterService = twitterService;
|
||||
_settings = settings.Value;
|
||||
_settings = settings;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
@ -35,39 +36,95 @@ namespace BirdsiteLive.Controllers
|
|||
{
|
||||
rel = "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
href = $"https://{_settings.Domain}/nodeinfo/2.0.json"
|
||||
},
|
||||
new Link()
|
||||
{
|
||||
rel = "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
href = $"https://{_settings.Domain}/nodeinfo/2.1.json"
|
||||
}
|
||||
}
|
||||
};
|
||||
return new JsonResult(nodeInfo);
|
||||
}
|
||||
|
||||
[Route("/nodeinfo/2.0.json")]
|
||||
public IActionResult NodeInfo()
|
||||
[Route("/nodeinfo/{id}.json")]
|
||||
public IActionResult NodeInfo(string id)
|
||||
{
|
||||
var nodeInfo = new NodeInfo
|
||||
{
|
||||
version = "2.0",
|
||||
usage = new Usage()
|
||||
{
|
||||
localPosts = 0,
|
||||
users = new Users()
|
||||
{
|
||||
total = 0
|
||||
}
|
||||
},
|
||||
software = new Software()
|
||||
{
|
||||
name = "BirdsiteLive",
|
||||
version = "0.1.0"
|
||||
},
|
||||
protocols = new []
|
||||
{
|
||||
"activitypub"
|
||||
},
|
||||
openRegistrations = false
|
||||
};
|
||||
var version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(3);
|
||||
|
||||
return new JsonResult(nodeInfo);
|
||||
if (id == "2.0")
|
||||
{
|
||||
var nodeInfo = new NodeInfoV20
|
||||
{
|
||||
version = "2.0",
|
||||
usage = new Usage()
|
||||
{
|
||||
localPosts = 0,
|
||||
users = new Users()
|
||||
{
|
||||
total = 0
|
||||
}
|
||||
},
|
||||
software = new Software()
|
||||
{
|
||||
name = "birdsitelive",
|
||||
version = version
|
||||
},
|
||||
protocols = new[]
|
||||
{
|
||||
"activitypub"
|
||||
},
|
||||
openRegistrations = false,
|
||||
services = new Models.WellKnownModels.Services()
|
||||
{
|
||||
inbound = new object[0],
|
||||
outbound = new object[0]
|
||||
},
|
||||
metadata = new Metadata()
|
||||
{
|
||||
email = _settings.AdminEmail
|
||||
}
|
||||
};
|
||||
return new JsonResult(nodeInfo);
|
||||
}
|
||||
if (id == "2.1")
|
||||
{
|
||||
var nodeInfo = new NodeInfoV21
|
||||
{
|
||||
version = "2.1",
|
||||
usage = new Usage()
|
||||
{
|
||||
localPosts = 0,
|
||||
users = new Users()
|
||||
{
|
||||
total = 0
|
||||
}
|
||||
},
|
||||
software = new SoftwareV21()
|
||||
{
|
||||
name = "birdsitelive",
|
||||
version = version,
|
||||
repository = "https://github.com/NicolasConstant/BirdsiteLive"
|
||||
},
|
||||
protocols = new[]
|
||||
{
|
||||
"activitypub"
|
||||
},
|
||||
openRegistrations = false,
|
||||
services = new Models.WellKnownModels.Services()
|
||||
{
|
||||
inbound = new object[0],
|
||||
outbound = new object[0]
|
||||
},
|
||||
metadata = new Metadata()
|
||||
{
|
||||
email = _settings.AdminEmail
|
||||
}
|
||||
};
|
||||
return new JsonResult(nodeInfo);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[Route("/.well-known/webfinger")]
|
||||
|
@ -130,63 +187,4 @@ namespace BirdsiteLive.Controllers
|
|||
return new JsonResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
public class WebFingerResult
|
||||
{
|
||||
public string subject { get; set; }
|
||||
public string[] aliases { get; set; }
|
||||
public List<WebFingerLink> links { get; set; } = new List<WebFingerLink>();
|
||||
}
|
||||
|
||||
public class WebFingerLink
|
||||
{
|
||||
public string rel { get; set; }
|
||||
public string type { get; set; }
|
||||
public string href { get; set; }
|
||||
}
|
||||
|
||||
public class WellKnownNodeInfo
|
||||
{
|
||||
public Link[] links { get; set; }
|
||||
}
|
||||
|
||||
public class Link
|
||||
{
|
||||
public string href { get; set; }
|
||||
public string rel { get; set; }
|
||||
}
|
||||
|
||||
public class NodeInfo
|
||||
{
|
||||
public string version { get; set; }
|
||||
public string[] protocols { get; set; }
|
||||
public Software software { get; set; }
|
||||
public Usage usage { get; set; }
|
||||
public bool openRegistrations { get; set; }
|
||||
public Services services { get; set; }
|
||||
public object metadata { get; set; }
|
||||
}
|
||||
|
||||
public class Services
|
||||
{
|
||||
public object[] inbound { get; set; }
|
||||
public object[] outbound { get; set; }
|
||||
}
|
||||
|
||||
public class Software
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string version { get; set; }
|
||||
}
|
||||
|
||||
public class Usage
|
||||
{
|
||||
public int localPosts { get; set; }
|
||||
public Users users { get; set; }
|
||||
}
|
||||
|
||||
public class Users
|
||||
{
|
||||
public int total { get; set; }
|
||||
}
|
||||
}
|
13
src/BirdsiteLive/Models/DisplayTwitterUser.cs
Normal file
13
src/BirdsiteLive/Models/DisplayTwitterUser.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace BirdsiteLive.Models
|
||||
{
|
||||
public class DisplayTwitterUser
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Acct { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string ProfileImageUrl { get; set; }
|
||||
|
||||
public string InstanceHandle { get; set; }
|
||||
}
|
||||
}
|
8
src/BirdsiteLive/Models/WellKnownModels/Link.cs
Normal file
8
src/BirdsiteLive/Models/WellKnownModels/Link.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class Link
|
||||
{
|
||||
public string href { get; set; }
|
||||
public string rel { get; set; }
|
||||
}
|
||||
}
|
7
src/BirdsiteLive/Models/WellKnownModels/Metadata.cs
Normal file
7
src/BirdsiteLive/Models/WellKnownModels/Metadata.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class Metadata
|
||||
{
|
||||
public string email { get; set; }
|
||||
}
|
||||
}
|
15
src/BirdsiteLive/Models/WellKnownModels/NodeInfoV20.cs
Normal file
15
src/BirdsiteLive/Models/WellKnownModels/NodeInfoV20.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class NodeInfoV20
|
||||
{
|
||||
public string version { get; set; }
|
||||
public string[] protocols { get; set; }
|
||||
public Software software { get; set; }
|
||||
public Usage usage { get; set; }
|
||||
public bool openRegistrations { get; set; }
|
||||
public Services services { get; set; }
|
||||
public Metadata metadata { get; set; }
|
||||
}
|
||||
}
|
13
src/BirdsiteLive/Models/WellKnownModels/NodeInfoV21.cs
Normal file
13
src/BirdsiteLive/Models/WellKnownModels/NodeInfoV21.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class NodeInfoV21
|
||||
{
|
||||
public string version { get; set; }
|
||||
public string[] protocols { get; set; }
|
||||
public Usage usage { get; set; }
|
||||
public bool openRegistrations { get; set; }
|
||||
public SoftwareV21 software { get; set; }
|
||||
public Services services { get; set; }
|
||||
public Metadata metadata { get; set; }
|
||||
}
|
||||
}
|
8
src/BirdsiteLive/Models/WellKnownModels/Services.cs
Normal file
8
src/BirdsiteLive/Models/WellKnownModels/Services.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class Services
|
||||
{
|
||||
public object[] inbound { get; set; }
|
||||
public object[] outbound { get; set; }
|
||||
}
|
||||
}
|
8
src/BirdsiteLive/Models/WellKnownModels/Software.cs
Normal file
8
src/BirdsiteLive/Models/WellKnownModels/Software.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class Software
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string version { get; set; }
|
||||
}
|
||||
}
|
9
src/BirdsiteLive/Models/WellKnownModels/SoftwareV21.cs
Normal file
9
src/BirdsiteLive/Models/WellKnownModels/SoftwareV21.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class SoftwareV21
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string repository { get; set; }
|
||||
public string version { get; set; }
|
||||
}
|
||||
}
|
8
src/BirdsiteLive/Models/WellKnownModels/Usage.cs
Normal file
8
src/BirdsiteLive/Models/WellKnownModels/Usage.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class Usage
|
||||
{
|
||||
public int localPosts { get; set; }
|
||||
public Users users { get; set; }
|
||||
}
|
||||
}
|
7
src/BirdsiteLive/Models/WellKnownModels/Users.cs
Normal file
7
src/BirdsiteLive/Models/WellKnownModels/Users.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class Users
|
||||
{
|
||||
public int total { get; set; }
|
||||
}
|
||||
}
|
9
src/BirdsiteLive/Models/WellKnownModels/WebFingerLink.cs
Normal file
9
src/BirdsiteLive/Models/WellKnownModels/WebFingerLink.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class WebFingerLink
|
||||
{
|
||||
public string rel { get; set; }
|
||||
public string type { get; set; }
|
||||
public string href { get; set; }
|
||||
}
|
||||
}
|
11
src/BirdsiteLive/Models/WellKnownModels/WebFingerResult.cs
Normal file
11
src/BirdsiteLive/Models/WellKnownModels/WebFingerResult.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class WebFingerResult
|
||||
{
|
||||
public string subject { get; set; }
|
||||
public string[] aliases { get; set; }
|
||||
public List<WebFingerLink> links { get; set; } = new List<WebFingerLink>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace BirdsiteLive.Models.WellKnownModels
|
||||
{
|
||||
public class WellKnownNodeInfo
|
||||
{
|
||||
public Link[] links { get; set; }
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.Services;
|
||||
using Lamar.Microsoft.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -23,6 +25,10 @@ namespace BirdsiteLive
|
|||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostedService<FederationService>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
44
src/BirdsiteLive/Services/FederationService.cs
Normal file
44
src/BirdsiteLive/Services/FederationService.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.DAL.Contracts;
|
||||
using BirdsiteLive.Pipeline;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BirdsiteLive.Services
|
||||
{
|
||||
public class FederationService : BackgroundService
|
||||
{
|
||||
private readonly IDbInitializerDal _dbInitializerDal;
|
||||
private readonly IStatusPublicationPipeline _statusPublicationPipeline;
|
||||
|
||||
#region Ctor
|
||||
public FederationService(IDbInitializerDal dbInitializerDal, IStatusPublicationPipeline statusPublicationPipeline)
|
||||
{
|
||||
_dbInitializerDal = dbInitializerDal;
|
||||
_statusPublicationPipeline = statusPublicationPipeline;
|
||||
}
|
||||
#endregion
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await DbInitAsync();
|
||||
await _statusPublicationPipeline.ExecuteAsync(stoppingToken);
|
||||
}
|
||||
|
||||
private async Task DbInitAsync()
|
||||
{
|
||||
var currentVersion = await _dbInitializerDal.GetCurrentDbVersionAsync();
|
||||
var mandatoryVersion = _dbInitializerDal.GetMandatoryDbVersion();
|
||||
|
||||
if (currentVersion == null)
|
||||
{
|
||||
await _dbInitializerDal.InitDbAsync();
|
||||
}
|
||||
else if (currentVersion != mandatoryVersion)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,10 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Common.Structs;
|
||||
using BirdsiteLive.DAL.Contracts;
|
||||
using BirdsiteLive.DAL.Postgres.DataAccessLayers;
|
||||
using BirdsiteLive.DAL.Postgres.Settings;
|
||||
using BirdsiteLive.Models;
|
||||
using Lamar;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
@ -34,7 +38,7 @@ namespace BirdsiteLive
|
|||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.Configure<InstanceSettings>(Configuration.GetSection("Instance"));
|
||||
//services.Configure<InstanceSettings>(Configuration.GetSection("Instance"));
|
||||
//services.Configure<TwitterSettings>(Configuration.GetSection("Twitter"));
|
||||
|
||||
services.AddControllersWithViews();
|
||||
|
@ -48,15 +52,39 @@ namespace BirdsiteLive
|
|||
var instanceSettings = Configuration.GetSection("Instance").Get<InstanceSettings>();
|
||||
services.For<InstanceSettings>().Use(x => instanceSettings);
|
||||
|
||||
var dbSettings = Configuration.GetSection("Db").Get<DbSettings>();
|
||||
services.For<DbSettings>().Use(x => dbSettings);
|
||||
|
||||
if (string.Equals(dbSettings.Type, DbTypes.Postgres, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var connString = $"Host={dbSettings.Host};Username={dbSettings.User};Password={dbSettings.Password};Database={dbSettings.Name}";
|
||||
var postgresSettings = new PostgresSettings
|
||||
{
|
||||
ConnString = connString
|
||||
};
|
||||
services.For<PostgresSettings>().Use(x => postgresSettings);
|
||||
|
||||
services.For<ITwitterUserDal>().Use<TwitterUserPostgresDal>().Singleton();
|
||||
services.For<IFollowersDal>().Use<FollowersPostgresDal>().Singleton();
|
||||
services.For<IDbInitializerDal>().Use<DbInitializerPostgresDal>().Singleton();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException($"{dbSettings.Type} is not supported");
|
||||
}
|
||||
|
||||
services.Scan(_ =>
|
||||
{
|
||||
_.Assembly("BirdsiteLive.Twitter");
|
||||
_.Assembly("BirdsiteLive.Domain");
|
||||
_.Assembly("BirdsiteLive.DAL");
|
||||
_.Assembly("BirdsiteLive.DAL.Postgres");
|
||||
_.Assembly("BirdsiteLive.Pipeline");
|
||||
_.TheCallingAssembly();
|
||||
|
||||
//_.AssemblyContainingType<IDal>();
|
||||
//_.Exclude(type => type.Name.Contains("Settings"));
|
||||
|
||||
|
||||
_.WithDefaultConventions();
|
||||
|
||||
_.LookForRegistries();
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
|
||||
<h1>Debug</h1>
|
||||
|
||||
<form asp-controller="Debug" asp-action="Follow" method="post">
|
||||
<form asp-controller="Debuging" asp-action="Follow" method="post">
|
||||
<!-- Input and Submit elements -->
|
||||
|
||||
<button type="submit" value="Submit">Follow</button>
|
||||
</form>
|
||||
|
||||
|
||||
<form asp-controller="Debug" asp-action="PostNote" method="post">
|
||||
<form asp-controller="Debuging" asp-action="PostNote" method="post">
|
||||
<!-- Input and Submit elements -->
|
||||
|
||||
<button type="submit" value="Submit">Post Note</button>
|
||||
|
|
|
@ -5,11 +5,28 @@
|
|||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Welcome</h1>
|
||||
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
|
||||
<p>
|
||||
<br />
|
||||
BirdsiteLIVE is a Twitter to ActivityPub bridge.<br />
|
||||
Find a Twitter account below:
|
||||
</p>
|
||||
|
||||
<form method="POST">
|
||||
@*<div class="form-group">
|
||||
<label for="exampleInputEmail1">Email address</label>
|
||||
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email">
|
||||
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
|
||||
</div>*@
|
||||
<div class="form-group">
|
||||
@*<label for="exampleInputPassword1">Password</label>*@
|
||||
<input type="text" class="form-control col-4 mx-auto" id="handle" name="handle" autocomplete="off" placeholder="Twitter Handle">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Show</button>
|
||||
</form>
|
||||
|
||||
|
||||
@if (HtmlHelperExtensions.IsDebug())
|
||||
@*@if (HtmlHelperExtensions.IsDebug())
|
||||
{
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Debuging" asp-action="Index">Debug</a>
|
||||
}
|
||||
}*@
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - BirdsiteLive</title>
|
||||
<title>@ViewData["Title"] - BirdsiteLIVE</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" />
|
||||
<link rel="stylesheet" href="~/css/birdsite.css" />
|
||||
|
@ -12,8 +12,8 @@
|
|||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">BirdsiteLive</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">BirdsiteLIVE</a>
|
||||
@*<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
@ -26,7 +26,7 @@
|
|||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>*@
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
@ -36,11 +36,14 @@
|
|||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="border-top footer text-muted">
|
||||
<div class="container">
|
||||
© 2020 - BirdsiteLive - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||
</div>
|
||||
</footer>
|
||||
<footer class="border-top footer text-muted">
|
||||
<div class="container">
|
||||
|
||||
<a href="https://github.com/NicolasConstant/BirdsiteLive">Github</a> @*<a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>*@
|
||||
|
||||
<span style="float: right;">BirdsiteLIVE @System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(3)</span>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
|
|
|
@ -1,18 +1,37 @@
|
|||
@model BirdsiteLive.Twitter.Models.TwitterUser
|
||||
@using Tweetinvi.Streams.Model.AccountActivity
|
||||
@model DisplayTwitterUser
|
||||
@{
|
||||
ViewData["Title"] = "User";
|
||||
}
|
||||
|
||||
<div class="profile" style="background-image: url('@ViewData.Model.ProfileBannerURL');">
|
||||
<div class="sub-profile">
|
||||
<img class="avatar" src="@ViewData.Model.ProfileImageUrl" />
|
||||
|
||||
<a href="@ViewData.Model.Url"><h1>@ViewData.Model.Name</h1></a>
|
||||
<h2>@@@ViewData.Model.Acct</h2>
|
||||
<div class="col-6 mx-auto">
|
||||
<a href="@ViewData.Model.Url" class="nounderline" title="@ViewData.Model.Url">
|
||||
@*<div class="profile" style="background-image: url('@ViewData.Model.ProfileBannerURL');">*@
|
||||
<div class="profile">
|
||||
|
||||
<div class="sub-profile">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24" class="logo-twitter r-13gxpu9 r-4qtqp9 r-yyyyoo r-6zzn7w r-19fsva8 r-dnmrzs r-bnwqim r-1plcrui r-lrvibr r-q1j0wu"><g><path d="M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.7-1.49 2.048-2.578-.9.534-1.897.922-2.958 1.13-.85-.904-2.06-1.47-3.4-1.47-2.572 0-4.658 2.086-4.658 4.66 0 .364.042.718.12 1.06-3.873-.195-7.304-2.05-9.602-4.868-.4.69-.63 1.49-.63 2.342 0 1.616.823 3.043 2.072 3.878-.764-.025-1.482-.234-2.11-.583v.06c0 2.257 1.605 4.14 3.737 4.568-.392.106-.803.162-1.227.162-.3 0-.593-.028-.877-.082.593 1.85 2.313 3.198 4.352 3.234-1.595 1.25-3.604 1.995-5.786 1.995-.376 0-.747-.022-1.112-.065 2.062 1.323 4.51 2.093 7.14 2.093 8.57 0 13.255-7.098 13.255-13.254 0-.2-.005-.402-.014-.602.91-.658 1.7-1.477 2.323-2.41z"></path></g></svg>
|
||||
</div>
|
||||
|
||||
<img class="avatar" src="@ViewData.Model.ProfileImageUrl" />
|
||||
|
||||
<h1>@ViewData.Model.Name <span class="handle">@@@ViewData.Model.Acct</span></h1>
|
||||
|
||||
@*<h2>@@@ViewData.Model.Acct</h2>*@
|
||||
|
||||
|
||||
<div class="description">
|
||||
@ViewData.Model.Description
|
||||
<div class="description">
|
||||
@ViewData.Model.Description
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<p>Search this handle to find it in your instance:</p>
|
||||
|
||||
<input type="text" name="textbox" value=" @ViewData.Model.InstanceHandle" onclick="this.select()" class="form-control" readonly/>
|
||||
</div>
|
|
@ -8,12 +8,18 @@
|
|||
},
|
||||
"AllowedHosts": "*",
|
||||
"Instance": {
|
||||
"Domain": "domain.name"
|
||||
"Domain": "domain.name",
|
||||
"AdminEmail": "me@domain.name"
|
||||
},
|
||||
"Db": {
|
||||
"Type": "postgres",
|
||||
"Host": "127.0.0.1",
|
||||
"Name": "mydb",
|
||||
"User": "username",
|
||||
"Password": "password"
|
||||
},
|
||||
"Twitter": {
|
||||
"ConsumerKey": "twitter.api.key",
|
||||
"ConsumerSecret": "twitter.api.key",
|
||||
"AccessToken": "twitter.api.key",
|
||||
"AccessTokenSecret": "twitter.api.key"
|
||||
"ConsumerSecret": "twitter.api.key"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +1,73 @@
|
|||
.profile {
|
||||
border: 1px #dddddd solid;
|
||||
border-radius: 5px;
|
||||
background-repeat: no-repeat;
|
||||
/*background-attachment: fixed;*/
|
||||
background-position: center;
|
||||
.nounderline {
|
||||
text-decoration: none !important
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.logo-twitter {
|
||||
filter: invert(51%) sepia(92%) saturate(1166%) hue-rotate(180deg) brightness(94%) contrast(98%);
|
||||
/*background: #349fef;*/
|
||||
}
|
||||
|
||||
.profile {
|
||||
border: 1px #dddddd solid;
|
||||
border-radius: 15px;
|
||||
/*background-repeat: no-repeat;*/
|
||||
/*background-attachment: fixed;*/
|
||||
/*background-position: center;*/
|
||||
color: black;
|
||||
}
|
||||
|
||||
.profile:hover {
|
||||
transition: all .2s;
|
||||
background-color: #f5f8fa;
|
||||
}
|
||||
|
||||
.profile h1 {
|
||||
font-size: 32px;
|
||||
margin-left: 120px;
|
||||
padding-top: 8px;
|
||||
font-size: 18px;
|
||||
margin-left: 60px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.profile h2 {
|
||||
font-size: 20px;
|
||||
margin-left: 120px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.profile a {
|
||||
/*.profile a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.profile a:hover {
|
||||
color: #555555;
|
||||
}
|
||||
.profile a:hover {
|
||||
color: #555555;
|
||||
}*/
|
||||
|
||||
.handle {
|
||||
color: gray;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.sub-profile {
|
||||
padding: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
padding: 10px 15px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/*.sub-profile a {
|
||||
color: black;
|
||||
}*/
|
||||
|
||||
.avatar {
|
||||
float: left;
|
||||
width: 100px;
|
||||
width: 50px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 40px;
|
||||
margin-left: 20px;
|
||||
font-weight: bold;
|
||||
margin-top: 0px;
|
||||
margin-left: 60px;
|
||||
/*font-weight: bold;*/
|
||||
}
|
||||
|
|
|
@ -419,6 +419,18 @@ h6, .h6 {
|
|||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.display-5 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 100;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.display-6 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
|
|
@ -106,8 +106,10 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
|||
followings INTEGER[],
|
||||
followingsSyncStatus JSONB,
|
||||
|
||||
acct VARCHAR(50),
|
||||
host VARCHAR(253),
|
||||
acct VARCHAR(50) NOT NULL,
|
||||
host VARCHAR(253) NOT NULL,
|
||||
inboxRoute VARCHAR(2048) NOT NULL,
|
||||
sharedInboxRoute VARCHAR(2048),
|
||||
UNIQUE (acct, host)
|
||||
);";
|
||||
await _tools.ExecuteRequestAsync(createFollowers);
|
||||
|
|
|
@ -20,8 +20,11 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
|||
}
|
||||
#endregion
|
||||
|
||||
public async Task CreateFollowerAsync(string acct, string host, int[] followings, Dictionary<int, long> followingSyncStatus)
|
||||
public async Task CreateFollowerAsync(string acct, string host, string inboxRoute, string sharedInboxRoute, int[] followings = null, Dictionary<int, long> followingSyncStatus = null)
|
||||
{
|
||||
if(followings == null) followings = new int[0];
|
||||
if(followingSyncStatus == null) followingSyncStatus = new Dictionary<int, long>();
|
||||
|
||||
var serializedDic = JsonConvert.SerializeObject(followingSyncStatus);
|
||||
|
||||
acct = acct.ToLowerInvariant();
|
||||
|
@ -32,8 +35,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
|||
dbConnection.Open();
|
||||
|
||||
await dbConnection.ExecuteAsync(
|
||||
$"INSERT INTO {_settings.FollowersTableName} (acct,host,followings,followingsSyncStatus) VALUES(@acct,@host,@followings, CAST(@followingsSyncStatus as json))",
|
||||
new { acct, host, followings, followingsSyncStatus = serializedDic });
|
||||
$"INSERT INTO {_settings.FollowersTableName} (acct,host,inboxRoute,sharedInboxRoute,followings,followingsSyncStatus) VALUES(@acct,@host,@inboxRoute,@sharedInboxRoute,@followings,CAST(@followingsSyncStatus as json))",
|
||||
new { acct, host, inboxRoute, sharedInboxRoute, followings, followingsSyncStatus = serializedDic });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,18 +71,19 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
|||
}
|
||||
}
|
||||
|
||||
public async Task UpdateFollowerAsync(int id, int[] followings, Dictionary<int, long> followingsSyncStatus)
|
||||
public async Task UpdateFollowerAsync(Follower follower)
|
||||
{
|
||||
if (id == default) throw new ArgumentException("id");
|
||||
if (follower == default) throw new ArgumentException("follower");
|
||||
if (follower.Id == default) throw new ArgumentException("id");
|
||||
|
||||
var serializedDic = JsonConvert.SerializeObject(followingsSyncStatus);
|
||||
var serializedDic = JsonConvert.SerializeObject(follower.FollowingsSyncStatus);
|
||||
var query = $"UPDATE {_settings.FollowersTableName} SET followings = @followings, followingsSyncStatus = CAST(@followingsSyncStatus as json) WHERE id = @id";
|
||||
|
||||
using (var dbConnection = Connection)
|
||||
{
|
||||
dbConnection.Open();
|
||||
|
||||
await dbConnection.QueryAsync(query, new { id, followings, followingsSyncStatus = serializedDic });
|
||||
await dbConnection.QueryAsync(query, new { follower.Id, follower.Followings, followingsSyncStatus = serializedDic });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +128,9 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
|||
Id = follower.Id,
|
||||
Acct = follower.Acct,
|
||||
Host = follower.Host,
|
||||
Followings = follower.Followings,
|
||||
InboxRoute = follower.InboxRoute,
|
||||
SharedInboxRoute = follower.SharedInboxRoute,
|
||||
Followings = follower.Followings.ToList(),
|
||||
FollowingsSyncStatus = JsonConvert.DeserializeObject<Dictionary<int,long>>(follower.FollowingsSyncStatus)
|
||||
};
|
||||
}
|
||||
|
@ -138,5 +144,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
|||
|
||||
public string Acct { get; set; }
|
||||
public string Host { get; set; }
|
||||
public string InboxRoute { get; set; }
|
||||
public string SharedInboxRoute { get; set; }
|
||||
}
|
||||
}
|
|
@ -4,9 +4,9 @@
|
|||
{
|
||||
public string ConnString { get; set; }
|
||||
|
||||
public string DbVersionTableName { get; set; } = "db-version";
|
||||
public string TwitterUserTableName { get; set; } = "twitter-users";
|
||||
public string DbVersionTableName { get; set; } = "db_version";
|
||||
public string TwitterUserTableName { get; set; } = "twitter_users";
|
||||
public string FollowersTableName { get; set; } = "followers";
|
||||
public string CachedTweetsTableName { get; set; } = "cached-tweets";
|
||||
public string CachedTweetsTableName { get; set; } = "cached_tweets";
|
||||
}
|
||||
}
|
|
@ -7,9 +7,10 @@ namespace BirdsiteLive.DAL.Contracts
|
|||
public interface IFollowersDal
|
||||
{
|
||||
Task<Follower> GetFollowerAsync(string acct, string host);
|
||||
Task CreateFollowerAsync(string acct, string host, int[] followings, Dictionary<int, long> followingSyncStatus);
|
||||
Task CreateFollowerAsync(string acct, string host, string inboxRoute, string sharedInboxRoute, int[] followings = null,
|
||||
Dictionary<int, long> followingSyncStatus = null);
|
||||
Task<Follower[]> GetFollowersAsync(int followedUserId);
|
||||
Task UpdateFollowerAsync(int id, int[] followings, Dictionary<int, long> followingSyncStatus);
|
||||
Task UpdateFollowerAsync(Follower follower);
|
||||
Task DeleteFollowerAsync(int id);
|
||||
Task DeleteFollowerAsync(string acct, string host);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@ namespace BirdsiteLive.DAL.Models
|
|||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int[] Followings { get; set; }
|
||||
public List<int> Followings { get; set; }
|
||||
public Dictionary<int, long> FollowingsSyncStatus { get; set; }
|
||||
|
||||
public string Acct { get; set; }
|
||||
public string Host { get; set; }
|
||||
public string InboxRoute { get; set; }
|
||||
public string SharedInboxRoute { get; set; }
|
||||
}
|
||||
}
|
|
@ -45,16 +45,52 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
|||
{19, 166L},
|
||||
{23, 167L}
|
||||
};
|
||||
var inboxRoute = "/myhandle/inbox";
|
||||
var sharedInboxRoute = "/inbox";
|
||||
|
||||
var dal = new FollowersPostgresDal(_settings);
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
|
||||
var result = await dal.GetFollowerAsync(acct, host);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(acct, result.Acct);
|
||||
Assert.AreEqual(host, result.Host);
|
||||
Assert.AreEqual(following.Length, result.Followings.Length);
|
||||
Assert.AreEqual(inboxRoute, result.InboxRoute);
|
||||
Assert.AreEqual(sharedInboxRoute, result.SharedInboxRoute);
|
||||
Assert.AreEqual(following.Length, result.Followings.Count);
|
||||
Assert.AreEqual(following[0], result.Followings[0]);
|
||||
Assert.AreEqual(followingSync.Count, result.FollowingsSyncStatus.Count);
|
||||
Assert.AreEqual(followingSync.First().Key, result.FollowingsSyncStatus.First().Key);
|
||||
Assert.AreEqual(followingSync.First().Value, result.FollowingsSyncStatus.First().Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CreateAndGetFollower_NoSharedInbox()
|
||||
{
|
||||
var acct = "myhandle";
|
||||
var host = "domain.ext";
|
||||
var following = new[] { 12, 19, 23 };
|
||||
var followingSync = new Dictionary<int, long>()
|
||||
{
|
||||
{12, 165L},
|
||||
{19, 166L},
|
||||
{23, 167L}
|
||||
};
|
||||
var inboxRoute = "/myhandle/inbox";
|
||||
string sharedInboxRoute = null;
|
||||
|
||||
var dal = new FollowersPostgresDal(_settings);
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
|
||||
var result = await dal.GetFollowerAsync(acct, host);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(acct, result.Acct);
|
||||
Assert.AreEqual(host, result.Host);
|
||||
Assert.AreEqual(inboxRoute, result.InboxRoute);
|
||||
Assert.AreEqual(sharedInboxRoute, result.SharedInboxRoute);
|
||||
Assert.AreEqual(following.Length, result.Followings.Count);
|
||||
Assert.AreEqual(following[0], result.Followings[0]);
|
||||
Assert.AreEqual(followingSync.Count, result.FollowingsSyncStatus.Count);
|
||||
Assert.AreEqual(followingSync.First().Key, result.FollowingsSyncStatus.First().Key);
|
||||
|
@ -71,19 +107,25 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
|||
var host = "domain.ext";
|
||||
var following = new[] { 1,2,3 };
|
||||
var followingSync = new Dictionary<int, long>();
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
var inboxRoute = "/myhandle1/inbox";
|
||||
var sharedInboxRoute = "/inbox";
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
|
||||
//User 2
|
||||
acct = "myhandle2";
|
||||
host = "domain.ext";
|
||||
following = new[] { 2, 4, 5 };
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
inboxRoute = "/myhandle2/inbox";
|
||||
sharedInboxRoute = "/inbox2";
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
|
||||
//User 2
|
||||
acct = "myhandle3";
|
||||
host = "domain.ext";
|
||||
following = new[] { 1 };
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
inboxRoute = "/myhandle3/inbox";
|
||||
sharedInboxRoute = "/inbox3";
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
|
||||
var result = await dal.GetFollowersAsync(2);
|
||||
Assert.AreEqual(2, result.Length);
|
||||
|
@ -107,24 +149,28 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
|||
{19, 166L},
|
||||
{23, 167L}
|
||||
};
|
||||
var inboxRoute = "/myhandle/inbox";
|
||||
var sharedInboxRoute = "/inbox";
|
||||
|
||||
var dal = new FollowersPostgresDal(_settings);
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
var result = await dal.GetFollowerAsync(acct, host);
|
||||
|
||||
var updatedFollowing = new[] { 12, 19, 23, 24 };
|
||||
var updatedFollowingSync = new Dictionary<int, long>()
|
||||
{
|
||||
var updatedFollowing = new List<int> { 12, 19, 23, 24 };
|
||||
var updatedFollowingSync = new Dictionary<int, long>(){
|
||||
{12, 170L},
|
||||
{19, 171L},
|
||||
{23, 172L},
|
||||
{24, 173L}
|
||||
};
|
||||
result.Followings = updatedFollowing.ToList();
|
||||
result.FollowingsSyncStatus = updatedFollowingSync;
|
||||
|
||||
|
||||
await dal.UpdateFollowerAsync(result.Id, updatedFollowing, updatedFollowingSync);
|
||||
await dal.UpdateFollowerAsync(result);
|
||||
result = await dal.GetFollowerAsync(acct, host);
|
||||
|
||||
Assert.AreEqual(updatedFollowing.Length, result.Followings.Length);
|
||||
Assert.AreEqual(updatedFollowing.Count, result.Followings.Count);
|
||||
Assert.AreEqual(updatedFollowing[0], result.Followings[0]);
|
||||
Assert.AreEqual(updatedFollowingSync.Count, result.FollowingsSyncStatus.Count);
|
||||
Assert.AreEqual(updatedFollowingSync.First().Key, result.FollowingsSyncStatus.First().Key);
|
||||
|
@ -143,9 +189,11 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
|||
{19, 166L},
|
||||
{23, 167L}
|
||||
};
|
||||
var inboxRoute = "/myhandle/inbox";
|
||||
var sharedInboxRoute = "/inbox";
|
||||
|
||||
var dal = new FollowersPostgresDal(_settings);
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
var result = await dal.GetFollowerAsync(acct, host);
|
||||
|
||||
var updatedFollowing = new[] { 12, 19 };
|
||||
|
@ -154,11 +202,13 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
|||
{12, 170L},
|
||||
{19, 171L}
|
||||
};
|
||||
result.Followings = updatedFollowing.ToList();
|
||||
result.FollowingsSyncStatus = updatedFollowingSync;
|
||||
|
||||
await dal.UpdateFollowerAsync(result.Id, updatedFollowing, updatedFollowingSync);
|
||||
await dal.UpdateFollowerAsync(result);
|
||||
result = await dal.GetFollowerAsync(acct, host);
|
||||
|
||||
Assert.AreEqual(updatedFollowing.Length, result.Followings.Length);
|
||||
Assert.AreEqual(updatedFollowing.Length, result.Followings.Count);
|
||||
Assert.AreEqual(updatedFollowing[0], result.Followings[0]);
|
||||
Assert.AreEqual(updatedFollowingSync.Count, result.FollowingsSyncStatus.Count);
|
||||
Assert.AreEqual(updatedFollowingSync.First().Key, result.FollowingsSyncStatus.First().Key);
|
||||
|
@ -177,9 +227,11 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
|||
{19, 166L},
|
||||
{23, 167L}
|
||||
};
|
||||
var inboxRoute = "/myhandle/inbox";
|
||||
var sharedInboxRoute = "/inbox";
|
||||
|
||||
var dal = new FollowersPostgresDal(_settings);
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
var result = await dal.GetFollowerAsync(acct, host);
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
|
@ -201,9 +253,11 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
|||
{19, 166L},
|
||||
{23, 167L}
|
||||
};
|
||||
var inboxRoute = "/myhandle/inbox";
|
||||
var sharedInboxRoute = "/inbox";
|
||||
|
||||
var dal = new FollowersPostgresDal(_settings);
|
||||
await dal.CreateFollowerAsync(acct, host, following, followingSync);
|
||||
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
|
||||
var result = await dal.GetFollowerAsync(acct, host);
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\BirdsiteLive.Domain\BirdsiteLive.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
48
src/Tests/BirdsiteLive.Domain.Tests/StatusServiceTests.cs
Normal file
48
src/Tests/BirdsiteLive.Domain.Tests/StatusServiceTests.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace BirdsiteLive.Domain.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class StatusServiceTests
|
||||
{
|
||||
private readonly InstanceSettings _settings;
|
||||
|
||||
#region Ctor
|
||||
public StatusServiceTests()
|
||||
{
|
||||
_settings = new InstanceSettings
|
||||
{
|
||||
Domain = "domain.name"
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
// [TestMethod]
|
||||
// public void ExtractMentionsTest()
|
||||
// {
|
||||
// #region Stubs
|
||||
// var username = "MyUserName";
|
||||
// var extractedTweet = new ExtractedTweet
|
||||
// {
|
||||
// Id = 124L,
|
||||
// CreatedAt = DateTime.UtcNow,
|
||||
// MessageContent = @"Getting ready for the weekend...have a great one everyone!
|
||||
//
|
||||
//Photo by Tim Tronckoe | @timtronckoe
|
||||
//
|
||||
//#archenemy #michaelamott #alissawhitegluz #jeffloomis #danielerlandsson #sharleedangelo"
|
||||
// };
|
||||
// #endregion
|
||||
|
||||
// var service = new StatusService(_settings);
|
||||
// var result = service.GetStatus(username, extractedTweet);
|
||||
|
||||
// #region Validations
|
||||
|
||||
// #endregion
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using BirdsiteLive.Common.Settings;
|
||||
using BirdsiteLive.Domain.Tools;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace BirdsiteLive.Domain.Tests.Tools
|
||||
{
|
||||
[TestClass]
|
||||
public class StatusExtractorTests
|
||||
{
|
||||
private readonly InstanceSettings _settings;
|
||||
|
||||
#region Ctor
|
||||
public StatusExtractorTests()
|
||||
{
|
||||
_settings = new InstanceSettings
|
||||
{
|
||||
Domain = "domain.name"
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_ReturnLines_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = "Bla.\n\n@Mention blo. https://t.co/pgtrJi9600";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.IsTrue(result.content.Contains("Bla."));
|
||||
Assert.IsTrue(result.content.Contains("</p><p>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_ReturnSingleLines_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = "Bla.\n@Mention blo. https://t.co/pgtrJi9600";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.IsTrue(result.content.Contains("Bla."));
|
||||
Assert.IsTrue(result.content.Contains("<br/>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_FormatUrl_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}https://t.co/L8BpyHgg25";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(0, result.tags.Length);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://t.co/L8BpyHgg25"" rel=""nofollow noopener noreferrer"" target=""_blank""><span class=""invisible"">https://</span><span class=""ellipsis"">t.co/L8BpyHgg25</span><span class=""invisible""></span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_FormatUrl_Long_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}https://www.eff.org/deeplinks/2020/07/pact-act-not-solution-problem-harmful-online-content";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(0, result.tags.Length);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://www.eff.org/deeplinks/2020/07/pact-act-not-solution-problem-harmful-online-content"" rel=""nofollow noopener noreferrer"" target=""_blank""><span class=""invisible"">https://www.</span><span class=""ellipsis"">eff.org/deeplinks/2020/07/pact</span><span class=""invisible"">-act-not-solution-problem-harmful-online-content</span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_FormatUrl_Exact_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}https://www.eff.org/deeplinks/2020/07/pact";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(0, result.tags.Length);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://www.eff.org/deeplinks/2020/07/pact"" rel=""nofollow noopener noreferrer"" target=""_blank""><span class=""invisible"">https://www.</span><span class=""ellipsis"">eff.org/deeplinks/2020/07/pact</span><span class=""invisible""></span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_MultiUrls__Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"https://t.co/L8BpyHgg25 Bla!{Environment.NewLine}https://www.eff.org/deeplinks/2020/07/pact-act-not-solution-problem-harmful-online-content";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(0, result.tags.Length);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://t.co/L8BpyHgg25"" rel=""nofollow noopener noreferrer"" target=""_blank""><span class=""invisible"">https://</span><span class=""ellipsis"">t.co/L8BpyHgg25</span><span class=""invisible""></span></a>"));
|
||||
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://www.eff.org/deeplinks/2020/07/pact-act-not-solution-problem-harmful-online-content"" rel=""nofollow noopener noreferrer"" target=""_blank""><span class=""invisible"">https://www.</span><span class=""ellipsis"">eff.org/deeplinks/2020/07/pact</span><span class=""invisible"">-act-not-solution-problem-harmful-online-content</span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_SingleHashTag_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}#mytag";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(1, result.tags.Length);
|
||||
Assert.AreEqual("#mytag", result.tags.First().name);
|
||||
Assert.AreEqual("Hashtag", result.tags.First().type);
|
||||
Assert.AreEqual("https://domain.name/tags/mytag", result.tags.First().href);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/mytag"" class=""mention hashtag"" rel=""tag"">#<span>mytag</span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_SingleHashTag_AtStart_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"#mytag Bla!";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(1, result.tags.Length);
|
||||
Assert.AreEqual("#mytag", result.tags.First().name);
|
||||
Assert.AreEqual("Hashtag", result.tags.First().type);
|
||||
Assert.AreEqual("https://domain.name/tags/mytag", result.tags.First().href);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/mytag"" class=""mention hashtag"" rel=""tag"">#<span>mytag</span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_SingleHashTag_SpecialChar_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}#COVIDー19";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(1, result.tags.Length);
|
||||
Assert.AreEqual("#COVIDー19", result.tags.First().name);
|
||||
Assert.AreEqual("Hashtag", result.tags.First().type);
|
||||
Assert.AreEqual("https://domain.name/tags/COVIDー19", result.tags.First().href);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/COVIDー19"" class=""mention hashtag"" rel=""tag"">#<span>COVIDー19</span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_MultiHashTags_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}#mytag #mytag2 #mytag3{Environment.NewLine}Test #bal Test";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(4, result.tags.Length);
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/mytag"" class=""mention hashtag"" rel=""tag"">#<span>mytag</span></a>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/mytag2"" class=""mention hashtag"" rel=""tag"">#<span>mytag2</span></a>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/mytag3"" class=""mention hashtag"" rel=""tag"">#<span>mytag3</span></a>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/bal"" class=""mention hashtag"" rel=""tag"">#<span>bal</span></a>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_SingleMentionTag_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}@mynickname";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(1, result.tags.Length);
|
||||
Assert.AreEqual("@mynickname@domain.name", result.tags.First().name);
|
||||
Assert.AreEqual("Mention", result.tags.First().type);
|
||||
Assert.AreEqual("https://domain.name/users/mynickname", result.tags.First().href);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@mynickname"" class=""u-url mention"">@<span>mynickname</span></a></span>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_SingleMentionTag_SpecialChar_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}@my___nickname";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(1, result.tags.Length);
|
||||
Assert.AreEqual("@my___nickname@domain.name", result.tags.First().name);
|
||||
Assert.AreEqual("Mention", result.tags.First().type);
|
||||
Assert.AreEqual("https://domain.name/users/my___nickname", result.tags.First().href);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@my___nickname"" class=""u-url mention"">@<span>my___nickname</span></a></span>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_SingleMentionTag_AtStart_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"@mynickname Bla!";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(1, result.tags.Length);
|
||||
Assert.AreEqual("@mynickname@domain.name", result.tags.First().name);
|
||||
Assert.AreEqual("Mention", result.tags.First().type);
|
||||
Assert.AreEqual("https://domain.name/users/mynickname", result.tags.First().href);
|
||||
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@mynickname"" class=""u-url mention"">@<span>mynickname</span></a></span>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_MultiMentionTag_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}@mynickname @mynickname2 @mynickname3{Environment.NewLine}Test @dada Test";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(4, result.tags.Length);
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@mynickname"" class=""u-url mention"">@<span>mynickname</span></a></span>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@mynickname2"" class=""u-url mention"">@<span>mynickname2</span></a></span>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@mynickname3"" class=""u-url mention"">@<span>mynickname3</span></a></span>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@dada"" class=""u-url mention"">@<span>dada</span></a></span>"));
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Extract_HeterogeneousTag_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var message = $"Bla!{Environment.NewLine}@mynickname #mytag2 @mynickname3{Environment.NewLine}Test @dada #dada Test";
|
||||
#endregion
|
||||
|
||||
var service = new StatusExtractor(_settings);
|
||||
var result = service.ExtractTags(message);
|
||||
|
||||
#region Validations
|
||||
Assert.AreEqual(5, result.tags.Length);
|
||||
Assert.IsTrue(result.content.Contains("Bla!"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@mynickname"" class=""u-url mention"">@<span>mynickname</span></a></span>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/mytag2"" class=""mention hashtag"" rel=""tag"">#<span>mytag2</span></a>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@mynickname3"" class=""u-url mention"">@<span>mynickname3</span></a></span>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<span class=""h-card""><a href=""https://domain.name/@dada"" class=""u-url mention"">@<span>dada</span></a></span>"));
|
||||
Assert.IsTrue(result.content.Contains(@"<a href=""https://domain.name/tags/dada"" class=""mention hashtag"" rel=""tag"">#<span>dada</span></a>"));
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.14.5" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\BirdsiteLive.Pipeline\BirdsiteLive.Pipeline.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,79 @@
|
|||
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.Models;
|
||||
using BirdsiteLive.Pipeline.Processors;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||
{
|
||||
[TestClass]
|
||||
public class RetrieveFollowersProcessorTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var userId1 = 1;
|
||||
var userId2 = 2;
|
||||
|
||||
var users = new List<UserWithTweetsToSync>
|
||||
{
|
||||
new UserWithTweetsToSync
|
||||
{
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Id = userId1
|
||||
}
|
||||
},
|
||||
new UserWithTweetsToSync
|
||||
{
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Id = userId2
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var followersUser1 = new List<Follower>
|
||||
{
|
||||
new Follower(),
|
||||
new Follower(),
|
||||
};
|
||||
var followersUser2 = new List<Follower>
|
||||
{
|
||||
new Follower(),
|
||||
new Follower(),
|
||||
new Follower(),
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||
followersDalMock
|
||||
.Setup(x => x.GetFollowersAsync(It.Is<int>(y => y == userId1)))
|
||||
.ReturnsAsync(followersUser1.ToArray());
|
||||
|
||||
followersDalMock
|
||||
.Setup(x => x.GetFollowersAsync(It.Is<int>(y => y == userId2)))
|
||||
.ReturnsAsync(followersUser2.ToArray());
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveFollowersProcessor(followersDalMock.Object);
|
||||
var result = (await processor.ProcessAsync(users.ToArray(), CancellationToken.None)).ToList();
|
||||
|
||||
#region Validations
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.Count);
|
||||
Assert.AreEqual(2, result.First(x => x.User.Id == userId1).Followers.Length);
|
||||
Assert.AreEqual(3, result.First(x => x.User.Id == userId2).Followers.Length);
|
||||
|
||||
followersDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.DAL.Contracts;
|
||||
using BirdsiteLive.DAL.Models;
|
||||
using BirdsiteLive.Pipeline.Processors;
|
||||
using BirdsiteLive.Twitter;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||
{
|
||||
[TestClass]
|
||||
public class RetrieveTweetsProcessorTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_UserNotSync_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var user1 = new SyncTwitterUser
|
||||
{
|
||||
Id = 1,
|
||||
Acct = "acct",
|
||||
LastTweetPostedId = -1
|
||||
};
|
||||
|
||||
var users = new[]
|
||||
{
|
||||
user1
|
||||
};
|
||||
|
||||
var tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = 47
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterServiceMock = new Mock<ITwitterService>(MockBehavior.Strict);
|
||||
twitterServiceMock
|
||||
.Setup(x => x.GetTimeline(
|
||||
It.Is<string>(y => y == user1.Acct),
|
||||
It.Is<int>(y => y == 1),
|
||||
It.Is<long>(y => y == -1)
|
||||
))
|
||||
.Returns(tweets);
|
||||
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
twitterUserDalMock
|
||||
.Setup(x => x.UpdateTwitterUserAsync(
|
||||
It.Is<int>(y => y == user1.Id),
|
||||
It.Is<long>(y => y == tweets.Last().Id),
|
||||
It.Is<long>(y => y == tweets.Last().Id)
|
||||
))
|
||||
.Returns(Task.CompletedTask);
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveTweetsProcessor(twitterServiceMock.Object, twitterUserDalMock.Object);
|
||||
var usersResult = await processor.ProcessAsync(users, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
twitterServiceMock.VerifyAll();
|
||||
twitterUserDalMock.VerifyAll();
|
||||
|
||||
Assert.AreEqual(0, usersResult.Length);
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_UserSync_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var user1 = new SyncTwitterUser
|
||||
{
|
||||
Id = 1,
|
||||
Acct = "acct",
|
||||
LastTweetPostedId = 46,
|
||||
LastTweetSynchronizedForAllFollowersId = 46
|
||||
};
|
||||
|
||||
var users = new[]
|
||||
{
|
||||
user1
|
||||
};
|
||||
|
||||
var tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = 47
|
||||
},
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = 48
|
||||
},
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = 49
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterServiceMock = new Mock<ITwitterService>(MockBehavior.Strict);
|
||||
twitterServiceMock
|
||||
.Setup(x => x.GetTimeline(
|
||||
It.Is<string>(y => y == user1.Acct),
|
||||
It.Is<int>(y => y == 200),
|
||||
It.Is<long>(y => y == user1.LastTweetSynchronizedForAllFollowersId)
|
||||
))
|
||||
.Returns(tweets);
|
||||
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveTweetsProcessor(twitterServiceMock.Object, twitterUserDalMock.Object);
|
||||
var usersResult = await processor.ProcessAsync(users, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
twitterServiceMock.VerifyAll();
|
||||
twitterUserDalMock.VerifyAll();
|
||||
|
||||
Assert.AreEqual(users.Length, usersResult.Length);
|
||||
Assert.AreEqual(users[0].Acct, usersResult[0].User.Acct);
|
||||
Assert.AreEqual(tweets.Length, usersResult[0].Tweets.Length);
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_UserPartiallySync_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var user1 = new SyncTwitterUser
|
||||
{
|
||||
Id = 1,
|
||||
Acct = "acct",
|
||||
LastTweetPostedId = 49,
|
||||
LastTweetSynchronizedForAllFollowersId = 46
|
||||
};
|
||||
|
||||
var users = new[]
|
||||
{
|
||||
user1
|
||||
};
|
||||
|
||||
var tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = 47
|
||||
},
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = 48
|
||||
},
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = 49
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterServiceMock = new Mock<ITwitterService>(MockBehavior.Strict);
|
||||
twitterServiceMock
|
||||
.Setup(x => x.GetTimeline(
|
||||
It.Is<string>(y => y == user1.Acct),
|
||||
It.Is<int>(y => y == 200),
|
||||
It.Is<long>(y => y == user1.LastTweetSynchronizedForAllFollowersId)
|
||||
))
|
||||
.Returns(tweets);
|
||||
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveTweetsProcessor(twitterServiceMock.Object, twitterUserDalMock.Object);
|
||||
var usersResult = await processor.ProcessAsync(users, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
twitterServiceMock.VerifyAll();
|
||||
twitterUserDalMock.VerifyAll();
|
||||
|
||||
Assert.AreEqual(users.Length, usersResult.Length);
|
||||
Assert.AreEqual(users[0].Acct, usersResult[0].User.Acct);
|
||||
Assert.AreEqual(tweets.Length, usersResult[0].Tweets.Length);
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
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.Processors;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||
{
|
||||
[TestClass]
|
||||
public class RetrieveTwitterUsersProcessorTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task GetTwitterUsersAsync_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var buffer = new BufferBlock<SyncTwitterUser[]>();
|
||||
var users = new[]
|
||||
{
|
||||
new SyncTwitterUser(),
|
||||
new SyncTwitterUser(),
|
||||
new SyncTwitterUser(),
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
twitterUserDalMock
|
||||
.Setup(x => x.GetAllTwitterUsersAsync())
|
||||
.ReturnsAsync(users);
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object);
|
||||
processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
|
||||
|
||||
await Task.Delay(50);
|
||||
|
||||
#region Validations
|
||||
twitterUserDalMock.VerifyAll();
|
||||
Assert.AreEqual(1, buffer.Count);
|
||||
buffer.TryReceive(out var result);
|
||||
Assert.AreEqual(3, result.Length);
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetTwitterUsersAsync_NoUsers_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var buffer = new BufferBlock<SyncTwitterUser[]>();
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
twitterUserDalMock
|
||||
.Setup(x => x.GetAllTwitterUsersAsync())
|
||||
.ReturnsAsync(new SyncTwitterUser[0]);
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object);
|
||||
processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
|
||||
|
||||
await Task.Delay(50);
|
||||
|
||||
#region Validations
|
||||
twitterUserDalMock.VerifyAll();
|
||||
Assert.AreEqual(0, buffer.Count);
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetTwitterUsersAsync_Exception_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var buffer = new BufferBlock<SyncTwitterUser[]>();
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
twitterUserDalMock
|
||||
.Setup(x => x.GetAllTwitterUsersAsync())
|
||||
.Throws(new Exception());
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object);
|
||||
var t = processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
|
||||
|
||||
await Task.WhenAny(t, Task.Delay(50));
|
||||
|
||||
#region Validations
|
||||
twitterUserDalMock.VerifyAll();
|
||||
Assert.AreEqual(0, buffer.Count);
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(OperationCanceledException))]
|
||||
public async Task GetTwitterUsersAsync_Cancellation_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var buffer = new BufferBlock<SyncTwitterUser[]>();
|
||||
var canTokenS = new CancellationTokenSource();
|
||||
canTokenS.Cancel();
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
#endregion
|
||||
|
||||
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object);
|
||||
await processor.GetTwitterUsersAsync(buffer, canTokenS.Token);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.DAL.Contracts;
|
||||
using BirdsiteLive.DAL.Models;
|
||||
using BirdsiteLive.Pipeline.Models;
|
||||
using BirdsiteLive.Pipeline.Processors;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Castle.DynamicProxy.Contributors;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||
{
|
||||
[TestClass]
|
||||
public class SaveProgressionProcessorTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var user = new SyncTwitterUser
|
||||
{
|
||||
Id = 1
|
||||
};
|
||||
var tweet1 = new ExtractedTweet
|
||||
{
|
||||
Id = 36
|
||||
};
|
||||
var tweet2 = new ExtractedTweet
|
||||
{
|
||||
Id = 37
|
||||
};
|
||||
var follower1 = new Follower
|
||||
{
|
||||
FollowingsSyncStatus = new Dictionary<int, long>
|
||||
{
|
||||
{1, 37}
|
||||
}
|
||||
};
|
||||
|
||||
var usersWithTweets = new UserWithTweetsToSync
|
||||
{
|
||||
Tweets = new []
|
||||
{
|
||||
tweet1,
|
||||
tweet2
|
||||
},
|
||||
Followers = new []
|
||||
{
|
||||
follower1
|
||||
},
|
||||
User = user
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
twitterUserDalMock
|
||||
.Setup(x => x.UpdateTwitterUserAsync(
|
||||
It.Is<int>(y => y == user.Id),
|
||||
It.Is<long>(y => y == tweet2.Id),
|
||||
It.Is<long>(y => y == tweet2.Id)
|
||||
))
|
||||
.Returns(Task.CompletedTask);
|
||||
#endregion
|
||||
|
||||
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object);
|
||||
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
twitterUserDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_PartiallySynchronized_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var user = new SyncTwitterUser
|
||||
{
|
||||
Id = 1
|
||||
};
|
||||
var tweet1 = new ExtractedTweet
|
||||
{
|
||||
Id = 36
|
||||
};
|
||||
var tweet2 = new ExtractedTweet
|
||||
{
|
||||
Id = 37
|
||||
};
|
||||
var tweet3 = new ExtractedTweet
|
||||
{
|
||||
Id = 38
|
||||
};
|
||||
var follower1 = new Follower
|
||||
{
|
||||
FollowingsSyncStatus = new Dictionary<int, long>
|
||||
{
|
||||
{1, 37}
|
||||
}
|
||||
};
|
||||
|
||||
var usersWithTweets = new UserWithTweetsToSync
|
||||
{
|
||||
Tweets = new[]
|
||||
{
|
||||
tweet1,
|
||||
tweet2,
|
||||
tweet3
|
||||
},
|
||||
Followers = new[]
|
||||
{
|
||||
follower1
|
||||
},
|
||||
User = user
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
twitterUserDalMock
|
||||
.Setup(x => x.UpdateTwitterUserAsync(
|
||||
It.Is<int>(y => y == user.Id),
|
||||
It.Is<long>(y => y == tweet3.Id),
|
||||
It.Is<long>(y => y == tweet2.Id)
|
||||
))
|
||||
.Returns(Task.CompletedTask);
|
||||
#endregion
|
||||
|
||||
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object);
|
||||
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
twitterUserDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_PartiallySynchronized_MultiUsers_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var user = new SyncTwitterUser
|
||||
{
|
||||
Id = 1
|
||||
};
|
||||
var tweet1 = new ExtractedTweet
|
||||
{
|
||||
Id = 36
|
||||
};
|
||||
var tweet2 = new ExtractedTweet
|
||||
{
|
||||
Id = 37
|
||||
};
|
||||
var tweet3 = new ExtractedTweet
|
||||
{
|
||||
Id = 38
|
||||
};
|
||||
var follower1 = new Follower
|
||||
{
|
||||
FollowingsSyncStatus = new Dictionary<int, long>
|
||||
{
|
||||
{1, 37}
|
||||
}
|
||||
};
|
||||
var follower2 = new Follower
|
||||
{
|
||||
FollowingsSyncStatus = new Dictionary<int, long>
|
||||
{
|
||||
{1, 38}
|
||||
}
|
||||
};
|
||||
|
||||
var usersWithTweets = new UserWithTweetsToSync
|
||||
{
|
||||
Tweets = new[]
|
||||
{
|
||||
tweet1,
|
||||
tweet2,
|
||||
tweet3
|
||||
},
|
||||
Followers = new[]
|
||||
{
|
||||
follower1,
|
||||
follower2
|
||||
},
|
||||
User = user
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||
twitterUserDalMock
|
||||
.Setup(x => x.UpdateTwitterUserAsync(
|
||||
It.Is<int>(y => y == user.Id),
|
||||
It.Is<long>(y => y == tweet3.Id),
|
||||
It.Is<long>(y => y == tweet2.Id)
|
||||
))
|
||||
.Returns(Task.CompletedTask);
|
||||
#endregion
|
||||
|
||||
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object);
|
||||
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
twitterUserDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,426 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.DAL.Models;
|
||||
using BirdsiteLive.Pipeline.Models;
|
||||
using BirdsiteLive.Pipeline.Processors;
|
||||
using BirdsiteLive.Pipeline.Processors.SubTasks;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||
{
|
||||
[TestClass]
|
||||
public class SendTweetsToFollowersProcessorTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_SameInstance_SharedInbox_OneTweet_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 1;
|
||||
var host = "domain.ext";
|
||||
var sharedInbox = "/inbox";
|
||||
var userId1 = 2;
|
||||
var userId2 = 3;
|
||||
var userAcct = "user";
|
||||
|
||||
var userWithTweets = new UserWithTweetsToSync()
|
||||
{
|
||||
Tweets = new []
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
}
|
||||
},
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Acct = userAcct
|
||||
},
|
||||
Followers = new []
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = userId1,
|
||||
Host = host,
|
||||
SharedInboxRoute = sharedInbox
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = userId2,
|
||||
Host = host,
|
||||
SharedInboxRoute = sharedInbox
|
||||
},
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||
|
||||
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||
sendTweetsToSharedInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<Follower[]>(y => y.Length == 2)))
|
||||
.Returns(Task.CompletedTask);
|
||||
#endregion
|
||||
|
||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object);
|
||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
sendTweetsToInboxTaskMock.VerifyAll();
|
||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_MultiInstances_SharedInbox_OneTweet_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 1;
|
||||
var host1 = "domain1.ext";
|
||||
var host2 = "domain2.ext";
|
||||
var sharedInbox = "/inbox";
|
||||
var userId1 = 2;
|
||||
var userId2 = 3;
|
||||
var userAcct = "user";
|
||||
|
||||
var userWithTweets = new UserWithTweetsToSync()
|
||||
{
|
||||
Tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
}
|
||||
},
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Acct = userAcct
|
||||
},
|
||||
Followers = new[]
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = userId1,
|
||||
Host = host1,
|
||||
SharedInboxRoute = sharedInbox
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = userId2,
|
||||
Host = host2,
|
||||
SharedInboxRoute = sharedInbox
|
||||
},
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||
|
||||
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||
foreach (var host in new [] { host1, host2})
|
||||
{
|
||||
sendTweetsToSharedInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<Follower[]>(y => y.Length == 1)))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
#endregion
|
||||
|
||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object);
|
||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
sendTweetsToInboxTaskMock.VerifyAll();
|
||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_MultiInstances_SharedInbox_OneTweet_Error_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 1;
|
||||
var host1 = "domain1.ext";
|
||||
var host2 = "domain2.ext";
|
||||
var sharedInbox = "/inbox";
|
||||
var userId1 = 2;
|
||||
var userId2 = 3;
|
||||
var userAcct = "user";
|
||||
|
||||
var userWithTweets = new UserWithTweetsToSync()
|
||||
{
|
||||
Tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
}
|
||||
},
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Acct = userAcct
|
||||
},
|
||||
Followers = new[]
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = userId1,
|
||||
Host = host1,
|
||||
SharedInboxRoute = sharedInbox
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = userId2,
|
||||
Host = host2,
|
||||
SharedInboxRoute = sharedInbox
|
||||
},
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||
|
||||
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||
sendTweetsToSharedInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct),
|
||||
It.Is<string>(y => y == host1),
|
||||
It.Is<Follower[]>(y => y.Length == 1)))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sendTweetsToSharedInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct),
|
||||
It.Is<string>(y => y == host2),
|
||||
It.Is<Follower[]>(y => y.Length == 1)))
|
||||
.Throws(new Exception());
|
||||
#endregion
|
||||
|
||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object);
|
||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
sendTweetsToInboxTaskMock.VerifyAll();
|
||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_SameInstance_Inbox_OneTweet_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 1;
|
||||
var host = "domain.ext";
|
||||
var inbox = "/user/inbox";
|
||||
var userId1 = 2;
|
||||
var userId2 = 3;
|
||||
var userAcct = "user";
|
||||
|
||||
var userWithTweets = new UserWithTweetsToSync()
|
||||
{
|
||||
Tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
}
|
||||
},
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Acct = userAcct
|
||||
},
|
||||
Followers = new[]
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = userId1,
|
||||
Host = host,
|
||||
InboxRoute = inbox
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = userId2,
|
||||
Host = host,
|
||||
InboxRoute = inbox
|
||||
},
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||
foreach (var userId in new[] { userId1, userId2 })
|
||||
{
|
||||
sendTweetsToInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<Follower>(y => y.Id == userId),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
|
||||
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||
#endregion
|
||||
|
||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object);
|
||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
sendTweetsToInboxTaskMock.VerifyAll();
|
||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 1;
|
||||
var host1 = "domain1.ext";
|
||||
var host2 = "domain2.ext";
|
||||
var inbox = "/user/inbox";
|
||||
var userId1 = 2;
|
||||
var userId2 = 3;
|
||||
var userAcct = "user";
|
||||
|
||||
var userWithTweets = new UserWithTweetsToSync()
|
||||
{
|
||||
Tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
}
|
||||
},
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Acct = userAcct
|
||||
},
|
||||
Followers = new[]
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = userId1,
|
||||
Host = host1,
|
||||
InboxRoute = inbox
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = userId2,
|
||||
Host = host2,
|
||||
InboxRoute = inbox
|
||||
},
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||
foreach (var userId in new[] { userId1, userId2 })
|
||||
{
|
||||
sendTweetsToInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<Follower>(y => y.Id == userId),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
|
||||
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||
#endregion
|
||||
|
||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object);
|
||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
sendTweetsToInboxTaskMock.VerifyAll();
|
||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Error_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 1;
|
||||
var host1 = "domain1.ext";
|
||||
var host2 = "domain2.ext";
|
||||
var inbox = "/user/inbox";
|
||||
var userId1 = 2;
|
||||
var userId2 = 3;
|
||||
var userAcct = "user";
|
||||
|
||||
var userWithTweets = new UserWithTweetsToSync()
|
||||
{
|
||||
Tweets = new[]
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
}
|
||||
},
|
||||
User = new SyncTwitterUser
|
||||
{
|
||||
Acct = userAcct
|
||||
},
|
||||
Followers = new[]
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = userId1,
|
||||
Host = host1,
|
||||
InboxRoute = inbox
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = userId2,
|
||||
Host = host2,
|
||||
InboxRoute = inbox
|
||||
},
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||
sendTweetsToInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<Follower>(y => y.Id == userId1),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sendTweetsToInboxTaskMock
|
||||
.Setup(x => x.ExecuteAsync(
|
||||
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||
It.Is<Follower>(y => y.Id == userId2),
|
||||
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||
.Throws(new Exception());
|
||||
|
||||
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||
#endregion
|
||||
|
||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object);
|
||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||
|
||||
#region Validations
|
||||
sendTweetsToInboxTaskMock.VerifyAll();
|
||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.ActivityPub.Models;
|
||||
using BirdsiteLive.DAL.Contracts;
|
||||
using BirdsiteLive.DAL.Models;
|
||||
using BirdsiteLive.Domain;
|
||||
using BirdsiteLive.Pipeline.Processors.SubTasks;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace BirdsiteLive.Pipeline.Tests.Processors.SubTasks
|
||||
{
|
||||
[TestClass]
|
||||
public class SendTweetsToInboxTaskTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task ExecuteAsync_SingleTweet_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 10;
|
||||
var tweets = new List<ExtractedTweet>
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId,
|
||||
}
|
||||
};
|
||||
|
||||
var noteId = "noteId";
|
||||
var note = new Note()
|
||||
{
|
||||
id = noteId
|
||||
};
|
||||
|
||||
var twitterHandle = "Test";
|
||||
var twitterUserId = 7;
|
||||
var twitterUser = new SyncTwitterUser
|
||||
{
|
||||
Id = twitterUserId,
|
||||
Acct = twitterHandle
|
||||
};
|
||||
|
||||
var host = "domain.ext";
|
||||
var inbox = "/user/inbox";
|
||||
var follower = new Follower
|
||||
{
|
||||
Id = 1,
|
||||
Host = host,
|
||||
InboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> { { twitterUserId, 9 } }
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var activityPubService = new Mock<IActivityPubService>(MockBehavior.Strict);
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == noteId),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.Accepted);
|
||||
|
||||
var statusServiceMock = new Mock<IStatusService>(MockBehavior.Strict);
|
||||
statusServiceMock
|
||||
.Setup(x => x.GetStatus(
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<ExtractedTweet>(y => y.Id == tweetId)))
|
||||
.Returns(note);
|
||||
|
||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||
followersDalMock
|
||||
.Setup(x => x.UpdateFollowerAsync(
|
||||
It.Is<Follower>(y => y.Id == follower.Id && y.FollowingsSyncStatus[twitterUserId] == tweetId)))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
#endregion
|
||||
|
||||
var task = new SendTweetsToInboxTask(activityPubService.Object, statusServiceMock.Object, followersDalMock.Object);
|
||||
await task.ExecuteAsync(tweets.ToArray(), follower, twitterUser);
|
||||
|
||||
#region Validations
|
||||
activityPubService.VerifyAll();
|
||||
statusServiceMock.VerifyAll();
|
||||
followersDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ExecuteAsync_MultipleTweets_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId1 = 10;
|
||||
var tweetId2 = 11;
|
||||
var tweetId3 = 12;
|
||||
var tweets = new List<ExtractedTweet>();
|
||||
foreach (var tweetId in new[] { tweetId1, tweetId2, tweetId3 })
|
||||
{
|
||||
tweets.Add(new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
});
|
||||
}
|
||||
|
||||
var twitterHandle = "Test";
|
||||
var twitterUserId = 7;
|
||||
var twitterUser = new SyncTwitterUser
|
||||
{
|
||||
Id = twitterUserId,
|
||||
Acct = twitterHandle
|
||||
};
|
||||
|
||||
var host = "domain.ext";
|
||||
var inbox = "/user/inbox";
|
||||
var follower = new Follower
|
||||
{
|
||||
Id = 1,
|
||||
Host = host,
|
||||
InboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> { { twitterUserId, 10 } }
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var activityPubService = new Mock<IActivityPubService>(MockBehavior.Strict);
|
||||
foreach (var tweetId in new[] { tweetId2, tweetId3 })
|
||||
{
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == tweetId.ToString()),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.Accepted);
|
||||
}
|
||||
|
||||
var statusServiceMock = new Mock<IStatusService>(MockBehavior.Strict);
|
||||
foreach (var tweetId in new[] { tweetId2, tweetId3 })
|
||||
{
|
||||
statusServiceMock
|
||||
.Setup(x => x.GetStatus(
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<ExtractedTweet>(y => y.Id == tweetId)))
|
||||
.Returns(new Note { id = tweetId.ToString() });
|
||||
}
|
||||
|
||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||
followersDalMock
|
||||
.Setup(x => x.UpdateFollowerAsync(
|
||||
It.Is<Follower>(y => y.Id == follower.Id && y.FollowingsSyncStatus[twitterUserId] == tweetId3)))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
#endregion
|
||||
|
||||
var task = new SendTweetsToInboxTask(activityPubService.Object, statusServiceMock.Object, followersDalMock.Object);
|
||||
await task.ExecuteAsync(tweets.ToArray(), follower, twitterUser);
|
||||
|
||||
#region Validations
|
||||
activityPubService.VerifyAll();
|
||||
statusServiceMock.VerifyAll();
|
||||
followersDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(Exception))]
|
||||
public async Task ExecuteAsync_MultipleTweets_Error_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId1 = 10;
|
||||
var tweetId2 = 11;
|
||||
var tweetId3 = 12;
|
||||
var tweets = new List<ExtractedTweet>();
|
||||
foreach (var tweetId in new[] { tweetId1, tweetId2, tweetId3 })
|
||||
{
|
||||
tweets.Add(new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
});
|
||||
}
|
||||
|
||||
var twitterHandle = "Test";
|
||||
var twitterUserId = 7;
|
||||
var twitterUser = new SyncTwitterUser
|
||||
{
|
||||
Id = twitterUserId,
|
||||
Acct = twitterHandle
|
||||
};
|
||||
|
||||
var host = "domain.ext";
|
||||
var inbox = "/user/inbox";
|
||||
var follower = new Follower
|
||||
{
|
||||
Id = 1,
|
||||
Host = host,
|
||||
InboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> { { twitterUserId, 10 } }
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var activityPubService = new Mock<IActivityPubService>(MockBehavior.Strict);
|
||||
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == tweetId2.ToString()),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId2.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.Accepted);
|
||||
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == tweetId3.ToString()),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId3.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.InternalServerError);
|
||||
|
||||
var statusServiceMock = new Mock<IStatusService>(MockBehavior.Strict);
|
||||
foreach (var tweetId in new[] { tweetId2, tweetId3 })
|
||||
{
|
||||
statusServiceMock
|
||||
.Setup(x => x.GetStatus(
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<ExtractedTweet>(y => y.Id == tweetId)))
|
||||
.Returns(new Note { id = tweetId.ToString() });
|
||||
}
|
||||
|
||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||
followersDalMock
|
||||
.Setup(x => x.UpdateFollowerAsync(
|
||||
It.Is<Follower>(y => y.Id == follower.Id && y.FollowingsSyncStatus[twitterUserId] == tweetId2)))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
#endregion
|
||||
|
||||
var task = new SendTweetsToInboxTask(activityPubService.Object, statusServiceMock.Object, followersDalMock.Object);
|
||||
|
||||
try
|
||||
{
|
||||
await task.ExecuteAsync(tweets.ToArray(), follower, twitterUser);
|
||||
}
|
||||
finally
|
||||
{
|
||||
#region Validations
|
||||
activityPubService.VerifyAll();
|
||||
statusServiceMock.VerifyAll();
|
||||
followersDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BirdsiteLive.ActivityPub.Models;
|
||||
using BirdsiteLive.DAL.Contracts;
|
||||
using BirdsiteLive.DAL.Models;
|
||||
using BirdsiteLive.Domain;
|
||||
using BirdsiteLive.Pipeline.Processors.SubTasks;
|
||||
using BirdsiteLive.Twitter.Models;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace BirdsiteLive.Pipeline.Tests.Processors.SubTasks
|
||||
{
|
||||
[TestClass]
|
||||
public class SendTweetsToSharedInboxTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task ExecuteAsync_SingleTweet_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId = 10;
|
||||
var tweets = new List<ExtractedTweet>
|
||||
{
|
||||
new ExtractedTweet
|
||||
{
|
||||
Id = tweetId,
|
||||
}
|
||||
};
|
||||
|
||||
var noteId = "noteId";
|
||||
var note = new Note()
|
||||
{
|
||||
id = noteId
|
||||
};
|
||||
|
||||
var twitterHandle = "Test";
|
||||
var twitterUserId = 7;
|
||||
var twitterUser = new SyncTwitterUser
|
||||
{
|
||||
Id = twitterUserId,
|
||||
Acct = twitterHandle
|
||||
};
|
||||
|
||||
var host = "domain.ext";
|
||||
var inbox = "/inbox";
|
||||
var followers = new List<Follower>
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = 1,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> { { twitterUserId, 9 } }
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = 2,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> { { twitterUserId, 8 } }
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = 3,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> { { twitterUserId, 7 } }
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var activityPubService = new Mock<IActivityPubService>(MockBehavior.Strict);
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == noteId),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.Accepted);
|
||||
|
||||
var statusServiceMock = new Mock<IStatusService>(MockBehavior.Strict);
|
||||
statusServiceMock
|
||||
.Setup(x => x.GetStatus(
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<ExtractedTweet>(y => y.Id == tweetId)))
|
||||
.Returns(note);
|
||||
|
||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||
|
||||
foreach (var follower in followers)
|
||||
{
|
||||
followersDalMock
|
||||
.Setup(x => x.UpdateFollowerAsync(
|
||||
It.Is<Follower>(y => y.Id == follower.Id && y.FollowingsSyncStatus[twitterUserId] == tweetId)))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
#endregion
|
||||
|
||||
var task = new SendTweetsToSharedInboxTask(activityPubService.Object, statusServiceMock.Object, followersDalMock.Object);
|
||||
await task.ExecuteAsync(tweets.ToArray(), twitterUser, host, followers.ToArray());
|
||||
|
||||
#region Validations
|
||||
activityPubService.VerifyAll();
|
||||
statusServiceMock.VerifyAll();
|
||||
followersDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ExecuteAsync_MultipleTweets_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId1 = 10;
|
||||
var tweetId2 = 11;
|
||||
var tweetId3 = 12;
|
||||
var tweets = new List<ExtractedTweet>();
|
||||
foreach (var tweetId in new[] { tweetId1, tweetId2, tweetId3 })
|
||||
{
|
||||
tweets.Add(new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
});
|
||||
}
|
||||
|
||||
var twitterHandle = "Test";
|
||||
var twitterUserId = 7;
|
||||
var twitterUser = new SyncTwitterUser
|
||||
{
|
||||
Id = twitterUserId,
|
||||
Acct = twitterHandle
|
||||
};
|
||||
|
||||
var host = "domain.ext";
|
||||
var inbox = "/inbox";
|
||||
var followers = new List<Follower>
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = 1,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> {{twitterUserId, 10}}
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = 2,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> {{twitterUserId, 8}}
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = 3,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> {{twitterUserId, 7}}
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var activityPubService = new Mock<IActivityPubService>(MockBehavior.Strict);
|
||||
foreach (var tweetId in new[] { tweetId2, tweetId3 })
|
||||
{
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == tweetId.ToString()),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.Accepted);
|
||||
}
|
||||
|
||||
var statusServiceMock = new Mock<IStatusService>(MockBehavior.Strict);
|
||||
foreach (var tweetId in new[] { tweetId2, tweetId3 })
|
||||
{
|
||||
statusServiceMock
|
||||
.Setup(x => x.GetStatus(
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<ExtractedTweet>(y => y.Id == tweetId)))
|
||||
.Returns(new Note { id = tweetId.ToString() });
|
||||
}
|
||||
|
||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||
|
||||
foreach (var follower in followers)
|
||||
{
|
||||
followersDalMock
|
||||
.Setup(x => x.UpdateFollowerAsync(
|
||||
It.Is<Follower>(y => y.Id == follower.Id && y.FollowingsSyncStatus[twitterUserId] == tweetId3)))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
#endregion
|
||||
|
||||
var task = new SendTweetsToSharedInboxTask(activityPubService.Object, statusServiceMock.Object, followersDalMock.Object);
|
||||
await task.ExecuteAsync(tweets.ToArray(), twitterUser, host, followers.ToArray());
|
||||
|
||||
#region Validations
|
||||
activityPubService.VerifyAll();
|
||||
statusServiceMock.VerifyAll();
|
||||
followersDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(Exception))]
|
||||
public async Task ExecuteAsync_MultipleTweets_Error_Test()
|
||||
{
|
||||
#region Stubs
|
||||
var tweetId1 = 10;
|
||||
var tweetId2 = 11;
|
||||
var tweetId3 = 12;
|
||||
var tweets = new List<ExtractedTweet>();
|
||||
foreach (var tweetId in new[] { tweetId1, tweetId2, tweetId3 })
|
||||
{
|
||||
tweets.Add(new ExtractedTweet
|
||||
{
|
||||
Id = tweetId
|
||||
});
|
||||
}
|
||||
|
||||
var twitterHandle = "Test";
|
||||
var twitterUserId = 7;
|
||||
var twitterUser = new SyncTwitterUser
|
||||
{
|
||||
Id = twitterUserId,
|
||||
Acct = twitterHandle
|
||||
};
|
||||
|
||||
var host = "domain.ext";
|
||||
var inbox = "/inbox";
|
||||
var followers = new List<Follower>
|
||||
{
|
||||
new Follower
|
||||
{
|
||||
Id = 1,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> {{twitterUserId, 10}}
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = 2,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> {{twitterUserId, 8}}
|
||||
},
|
||||
new Follower
|
||||
{
|
||||
Id = 3,
|
||||
Host = host,
|
||||
SharedInboxRoute = inbox,
|
||||
FollowingsSyncStatus = new Dictionary<int, long> {{twitterUserId, 7}}
|
||||
}
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Mocks
|
||||
var activityPubService = new Mock<IActivityPubService>(MockBehavior.Strict);
|
||||
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == tweetId2.ToString()),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId2.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.Accepted);
|
||||
|
||||
activityPubService
|
||||
.Setup(x => x.PostNewNoteActivity(
|
||||
It.Is<Note>(y => y.id == tweetId3.ToString()),
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<string>(y => y == tweetId3.ToString()),
|
||||
It.Is<string>(y => y == host),
|
||||
It.Is<string>(y => y == inbox)))
|
||||
.ReturnsAsync(HttpStatusCode.InternalServerError);
|
||||
|
||||
var statusServiceMock = new Mock<IStatusService>(MockBehavior.Strict);
|
||||
foreach (var tweetId in new[] { tweetId2, tweetId3 })
|
||||
{
|
||||
statusServiceMock
|
||||
.Setup(x => x.GetStatus(
|
||||
It.Is<string>(y => y == twitterHandle),
|
||||
It.Is<ExtractedTweet>(y => y.Id == tweetId)))
|
||||
.Returns(new Note { id = tweetId.ToString() });
|
||||
}
|
||||
|
||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||
|
||||
foreach (var follower in followers)
|
||||
{
|
||||
followersDalMock
|
||||
.Setup(x => x.UpdateFollowerAsync(
|
||||
It.Is<Follower>(y => y.Id == follower.Id && y.FollowingsSyncStatus[twitterUserId] == tweetId2)))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
#endregion
|
||||
|
||||
var task = new SendTweetsToSharedInboxTask(activityPubService.Object, statusServiceMock.Object, followersDalMock.Object);
|
||||
|
||||
try
|
||||
{
|
||||
await task.ExecuteAsync(tweets.ToArray(), twitterUser, host, followers.ToArray());
|
||||
}
|
||||
finally
|
||||
{
|
||||
#region Validations
|
||||
activityPubService.VerifyAll();
|
||||
statusServiceMock.VerifyAll();
|
||||
followersDalMock.VerifyAll();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue