diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml
index 9e68110..b1fc70d 100644
--- a/.github/workflows/dotnet-core.yml
+++ b/.github/workflows/dotnet-core.yml
@@ -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:
diff --git a/src/BirdsiteLive/Dockerfile b/Dockerfile
similarity index 59%
rename from src/BirdsiteLive/Dockerfile
rename to Dockerfile
index 7945589..0dcccda 100644
--- a/src/BirdsiteLive/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..77dee53
--- /dev/null
+++ b/docker-compose.yml
@@ -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
\ No newline at end of file
diff --git a/src/BirdsiteLive.ActivityPub/ApDeserializer.cs b/src/BirdsiteLive.ActivityPub/ApDeserializer.cs
index 1eaffa8..e577fbb 100644
--- a/src/BirdsiteLive.ActivityPub/ApDeserializer.cs
+++ b/src/BirdsiteLive.ActivityPub/ApDeserializer.cs
@@ -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(json);
- switch (activity.type)
+ try
{
- case "Follow":
- return JsonConvert.DeserializeObject(json);
- case "Undo":
- var a = JsonConvert.DeserializeObject(json);
- if(a.apObject.type == "Follow")
- return JsonConvert.DeserializeObject(json);
- break;
- case "Accept":
- var accept = JsonConvert.DeserializeObject(json);
- //var acceptType = JsonConvert.DeserializeObject(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(json);
+ switch (activity.type)
+ {
+ case "Follow":
+ return JsonConvert.DeserializeObject(json);
+ case "Undo":
+ var a = JsonConvert.DeserializeObject(json);
+ if(a.apObject.type == "Follow")
+ return JsonConvert.DeserializeObject(json);
+ break;
+ case "Accept":
+ var accept = JsonConvert.DeserializeObject(json);
+ //var acceptType = JsonConvert.DeserializeObject(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;
diff --git a/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj b/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj
index 8dfebd7..a690b63 100644
--- a/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj
+++ b/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptUndoFollow.cs b/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptUndoFollow.cs
new file mode 100644
index 0000000..9453f25
--- /dev/null
+++ b/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptUndoFollow.cs
@@ -0,0 +1,10 @@
+using Newtonsoft.Json;
+
+namespace BirdsiteLive.ActivityPub
+{
+ public class ActivityAcceptUndoFollow : Activity
+ {
+ [JsonProperty("object")]
+ public ActivityUndoFollow apObject { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.ActivityPub/Models/ActivityCreateNote.cs b/src/BirdsiteLive.ActivityPub/Models/ActivityCreateNote.cs
index d100b3a..fe14ac7 100644
--- a/src/BirdsiteLive.ActivityPub/Models/ActivityCreateNote.cs
+++ b/src/BirdsiteLive.ActivityPub/Models/ActivityCreateNote.cs
@@ -1,4 +1,5 @@
using System;
+using BirdsiteLive.ActivityPub.Models;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
diff --git a/src/BirdsiteLive.ActivityPub/Models/Actor.cs b/src/BirdsiteLive.ActivityPub/Models/Actor.cs
index 59ea71f..0552f25 100644
--- a/src/BirdsiteLive.ActivityPub/Models/Actor.cs
+++ b/src/BirdsiteLive.ActivityPub/Models/Actor.cs
@@ -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; }
}
}
diff --git a/src/BirdsiteLive.ActivityPub/Models/Attachment.cs b/src/BirdsiteLive.ActivityPub/Models/Attachment.cs
new file mode 100644
index 0000000..d7b86dd
--- /dev/null
+++ b/src/BirdsiteLive.ActivityPub/Models/Attachment.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.ActivityPub/Models/EndPoints.cs b/src/BirdsiteLive.ActivityPub/Models/EndPoints.cs
new file mode 100644
index 0000000..8d671d5
--- /dev/null
+++ b/src/BirdsiteLive.ActivityPub/Models/EndPoints.cs
@@ -0,0 +1,7 @@
+namespace BirdsiteLive.ActivityPub
+{
+ public class EndPoints
+ {
+ public string sharedInbox { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.ActivityPub/Models/Followers.cs b/src/BirdsiteLive.ActivityPub/Models/Followers.cs
new file mode 100644
index 0000000..85c44d2
--- /dev/null
+++ b/src/BirdsiteLive.ActivityPub/Models/Followers.cs
@@ -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";
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.ActivityPub/Models/Note.cs b/src/BirdsiteLive.ActivityPub/Models/Note.cs
index cc3d561..fc6dc5b 100644
--- a/src/BirdsiteLive.ActivityPub/Models/Note.cs
+++ b/src/BirdsiteLive.ActivityPub/Models/Note.cs
@@ -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 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 replies;
}
}
\ No newline at end of file
diff --git a/src/BirdsiteLive.ActivityPub/Models/Tag.cs b/src/BirdsiteLive.ActivityPub/Models/Tag.cs
new file mode 100644
index 0000000..0699c97
--- /dev/null
+++ b/src/BirdsiteLive.ActivityPub/Models/Tag.cs
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Common/Settings/DbSettings.cs b/src/BirdsiteLive.Common/Settings/DbSettings.cs
new file mode 100644
index 0000000..b70fba1
--- /dev/null
+++ b/src/BirdsiteLive.Common/Settings/DbSettings.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Common/Settings/InstanceSettings.cs b/src/BirdsiteLive.Common/Settings/InstanceSettings.cs
index aabe822..ba2a517 100644
--- a/src/BirdsiteLive.Common/Settings/InstanceSettings.cs
+++ b/src/BirdsiteLive.Common/Settings/InstanceSettings.cs
@@ -3,5 +3,6 @@
public class InstanceSettings
{
public string Domain { get; set; }
+ public string AdminEmail { get; set; }
}
}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Common/Settings/TwitterSettings.cs b/src/BirdsiteLive.Common/Settings/TwitterSettings.cs
index d0970f2..3e9095a 100644
--- a/src/BirdsiteLive.Common/Settings/TwitterSettings.cs
+++ b/src/BirdsiteLive.Common/Settings/TwitterSettings.cs
@@ -4,7 +4,5 @@
{
public string ConsumerKey { get; set; }
public string ConsumerSecret { get; set; }
- public string AccessToken { get; set; }
- public string AccessTokenSecret { get; set; }
}
}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Common/Structs/DbTypes.cs b/src/BirdsiteLive.Common/Structs/DbTypes.cs
new file mode 100644
index 0000000..767f0f3
--- /dev/null
+++ b/src/BirdsiteLive.Common/Structs/DbTypes.cs
@@ -0,0 +1,7 @@
+namespace BirdsiteLive.Common.Structs
+{
+ public struct DbTypes
+ {
+ public static string Postgres = "postgres";
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Domain/ActivityPubService.cs b/src/BirdsiteLive.Domain/ActivityPubService.cs
index d30daf8..bbabf07 100644
--- a/src/BirdsiteLive.Domain/ActivityPubService.cs
+++ b/src/BirdsiteLive.Domain/ActivityPubService.cs
@@ -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 GetUser(string objectId);
Task PostDataAsync(T data, string targetHost, string actorUrl, string inbox = null);
+ Task 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 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 PostDataAsync(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;
}
+
+
}
}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Domain/BirdsiteLive.Domain.csproj b/src/BirdsiteLive.Domain/BirdsiteLive.Domain.csproj
index 50eb4d2..cb89578 100644
--- a/src/BirdsiteLive.Domain/BirdsiteLive.Domain.csproj
+++ b/src/BirdsiteLive.Domain/BirdsiteLive.Domain.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/BirdsiteLive.Domain/BusinessUseCases/ProcessFollowUser.cs b/src/BirdsiteLive.Domain/BusinessUseCases/ProcessFollowUser.cs
new file mode 100644
index 0000000..ac657e4
--- /dev/null
+++ b/src/BirdsiteLive.Domain/BusinessUseCases/ProcessFollowUser.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Domain/BusinessUseCases/ProcessUnfollowUser.cs b/src/BirdsiteLive.Domain/BusinessUseCases/ProcessUnfollowUser.cs
new file mode 100644
index 0000000..4d5483a
--- /dev/null
+++ b/src/BirdsiteLive.Domain/BusinessUseCases/ProcessUnfollowUser.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Domain/CryptoService.cs b/src/BirdsiteLive.Domain/CryptoService.cs
index ed62a59..01e7d63 100644
--- a/src/BirdsiteLive.Domain/CryptoService.cs
+++ b/src/BirdsiteLive.Domain/CryptoService.cs
@@ -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
/// in the form of https://domain.io/actor
/// in the form of domain.io
///
- 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);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Domain/StatusService.cs b/src/BirdsiteLive.Domain/StatusService.cs
new file mode 100644
index 0000000..54a95ca
--- /dev/null
+++ b/src/BirdsiteLive.Domain/StatusService.cs
@@ -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 = $"{extractedTags.content}
",
+ 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Domain/Tools/StatusExtractor.cs b/src/BirdsiteLive.Domain/Tools/StatusExtractor.cs
new file mode 100644
index 0000000..d78bf6e
--- /dev/null
+++ b/src/BirdsiteLive.Domain/Tools/StatusExtractor.cs
@@ -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();
+ messageContent = $" {messageContent} ";
+
+ // Replace return lines
+ messageContent = Regex.Replace(messageContent, @"\r\n\r\n?|\n\n", "
");
+ messageContent = Regex.Replace(messageContent, @"\r\n?|\n", "
");
+
+ // 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(),
+ $@" {protocol}{firstPart}{secondPart}");
+ }
+
+ // 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(),
+ $@" #{tag}");
+ }
+
+ // 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(),
+ $@" @{mention}");
+ }
+
+ // Clean up return lines
+ messageContent = Regex.Replace(messageContent, @"
", "
");
+ messageContent = Regex.Replace(messageContent, @"
", "
");
+
+ return (messageContent.Trim(), tags.ToArray());
+ }
+
+ private IEnumerable OrderByLength(MatchCollection matches)
+ {
+ var result = new List();
+
+ foreach (Match m in matches) result.Add(m);
+ result = result.OrderByDescending(x => x.Length).ToList();
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Domain/UserService.cs b/src/BirdsiteLive.Domain/UserService.cs
index d084e97..221ff4f 100644
--- a/src/BirdsiteLive.Domain/UserService.cs
+++ b/src/BirdsiteLive.Domain/UserService.cs
@@ -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 FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity);
- Note GetStatus(TwitterUser user, ITweet tweet);
+ Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity, string body);
+ Task UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary 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 = $"{tweet.Text}
",
- attachment = new string[0],
- tag = new string[0]
- };
- return note;
- }
-
- public async Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity)
+ public async Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary 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 ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary 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 UndoFollowRequestedAsync(string signature, string method, string path, string queryString,
+ Dictionary 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 ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary 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();
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; }
+ }
}
diff --git a/src/BirdsiteLive.Pipeline/BirdsiteLive.Pipeline.csproj b/src/BirdsiteLive.Pipeline/BirdsiteLive.Pipeline.csproj
new file mode 100644
index 0000000..6b8b510
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/BirdsiteLive.Pipeline.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netstandard2.0
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BirdsiteLive.Pipeline/Contracts/IRetrieveFollowersProcessor.cs b/src/BirdsiteLive.Pipeline/Contracts/IRetrieveFollowersProcessor.cs
new file mode 100644
index 0000000..e0d45dc
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Contracts/IRetrieveFollowersProcessor.cs
@@ -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> ProcessAsync(UserWithTweetsToSync[] userWithTweetsToSyncs, CancellationToken ct);
+ //IAsyncEnumerable ProcessAsync(UserWithTweetsToSync[] userWithTweetsToSyncs, CancellationToken ct);
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Contracts/IRetrieveTweetsProcessor.cs b/src/BirdsiteLive.Pipeline/Contracts/IRetrieveTweetsProcessor.cs
new file mode 100644
index 0000000..451f1d1
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Contracts/IRetrieveTweetsProcessor.cs
@@ -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 ProcessAsync(SyncTwitterUser[] syncTwitterUsers, CancellationToken ct);
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Contracts/IRetrieveTwitterUsersProcessor.cs b/src/BirdsiteLive.Pipeline/Contracts/IRetrieveTwitterUsersProcessor.cs
new file mode 100644
index 0000000..b71ae93
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Contracts/IRetrieveTwitterUsersProcessor.cs
@@ -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 twitterUsersBufferBlock, CancellationToken ct);
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Contracts/ISaveProgressionProcessor.cs b/src/BirdsiteLive.Pipeline/Contracts/ISaveProgressionProcessor.cs
new file mode 100644
index 0000000..02efaef
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Contracts/ISaveProgressionProcessor.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Contracts/ISendTweetsToFollowersProcessor.cs b/src/BirdsiteLive.Pipeline/Contracts/ISendTweetsToFollowersProcessor.cs
new file mode 100644
index 0000000..6d55957
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Contracts/ISendTweetsToFollowersProcessor.cs
@@ -0,0 +1,11 @@
+using System.Threading;
+using System.Threading.Tasks;
+using BirdsiteLive.Pipeline.Models;
+
+namespace BirdsiteLive.Pipeline.Contracts
+{
+ public interface ISendTweetsToFollowersProcessor
+ {
+ Task ProcessAsync(UserWithTweetsToSync userWithTweetsToSync, CancellationToken ct);
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Models/UserWithTweetsToSync.cs b/src/BirdsiteLive.Pipeline/Models/UserWithTweetsToSync.cs
new file mode 100644
index 0000000..57810c7
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Models/UserWithTweetsToSync.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Processors/RetrieveFollowersProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/RetrieveFollowersProcessor.cs
new file mode 100644
index 0000000..4b2f150
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Processors/RetrieveFollowersProcessor.cs
@@ -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> 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Processors/RetrieveTweetsProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/RetrieveTweetsProcessor.cs
new file mode 100644
index 0000000..68ca0b0
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Processors/RetrieveTweetsProcessor.cs
@@ -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 ProcessAsync(SyncTwitterUser[] syncTwitterUsers, CancellationToken ct)
+ {
+ var usersWtTweets = new List();
+
+ //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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Processors/RetrieveTwitterUsersProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/RetrieveTwitterUsersProcessor.cs
new file mode 100644
index 0000000..f8ea2a2
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Processors/RetrieveTwitterUsersProcessor.cs
@@ -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 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Processors/SaveProgressionProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/SaveProgressionProcessor.cs
new file mode 100644
index 0000000..5b305e7
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Processors/SaveProgressionProcessor.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Processors/SendTweetsToFollowersProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/SendTweetsToFollowersProcessor.cs
new file mode 100644
index 0000000..95fd0c8
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Processors/SendTweetsToFollowersProcessor.cs
@@ -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 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 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 followerWtInbox, SyncTwitterUser user)
+ {
+ foreach (var follower in followerWtInbox)
+ {
+ try
+ {
+ await _sendTweetsToInboxTask.ExecuteAsync(tweets, follower, user);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ //TODO handle error
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Processors/SubTasks/SendTweetsToInboxTask.cs b/src/BirdsiteLive.Pipeline/Processors/SubTasks/SendTweetsToInboxTask.cs
new file mode 100644
index 0000000..eb1fb36
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Processors/SubTasks/SendTweetsToInboxTask.cs
@@ -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 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 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);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/Processors/SubTasks/SendTweetsToSharedInboxTask.cs b/src/BirdsiteLive.Pipeline/Processors/SubTasks/SendTweetsToSharedInboxTask.cs
new file mode 100644
index 0000000..bdebdcd
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/Processors/SubTasks/SendTweetsToSharedInboxTask.cs
@@ -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);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Pipeline/StatusPublicationPipeline.cs b/src/BirdsiteLive.Pipeline/StatusPublicationPipeline.cs
new file mode 100644
index 0000000..bcb896f
--- /dev/null
+++ b/src/BirdsiteLive.Pipeline/StatusPublicationPipeline.cs
@@ -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(new DataflowBlockOptions { BoundedCapacity = 1, CancellationToken = ct});
+ var retrieveTweetsBlock = new TransformBlock(async x => await _retrieveTweetsProcessor.ProcessAsync(x, ct));
+ var retrieveTweetsBufferBlock = new BufferBlock(new DataflowBlockOptions { BoundedCapacity = 1, CancellationToken = ct });
+ var retrieveFollowersBlock = new TransformManyBlock(async x => await _retrieveFollowersProcessor.ProcessAsync(x, ct));
+ var retrieveFollowersBufferBlock = new BufferBlock(new DataflowBlockOptions { BoundedCapacity = 20, CancellationToken = ct });
+ var sendTweetsToFollowersBlock = new ActionBlock(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;
+ }
+ }
+}
diff --git a/src/BirdsiteLive.Twitter/Extractors/TweetExtractor.cs b/src/BirdsiteLive.Twitter/Extractors/TweetExtractor.cs
new file mode 100644
index 0000000..2cb7dde
--- /dev/null
+++ b/src/BirdsiteLive.Twitter/Extractors/TweetExtractor.cs
@@ -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 media)
+ {
+ var result = new List();
+
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Twitter/Models/ExtractedMedia.cs b/src/BirdsiteLive.Twitter/Models/ExtractedMedia.cs
new file mode 100644
index 0000000..cdab034
--- /dev/null
+++ b/src/BirdsiteLive.Twitter/Models/ExtractedMedia.cs
@@ -0,0 +1,8 @@
+namespace BirdsiteLive.Twitter.Models
+{
+ public class ExtractedMedia
+ {
+ public string MediaType { get; set; }
+ public string Url { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Twitter/Models/ExtractedTweet.cs b/src/BirdsiteLive.Twitter/Models/ExtractedTweet.cs
new file mode 100644
index 0000000..0363973
--- /dev/null
+++ b/src/BirdsiteLive.Twitter/Models/ExtractedTweet.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Twitter/TwitterService.cs b/src/BirdsiteLive.Twitter/TwitterService.cs
index 3bec34a..f49f089 100644
--- a/src/BirdsiteLive.Twitter/TwitterService.cs
+++ b/src/BirdsiteLive.Twitter/TwitterService.cs
@@ -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();
+ 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();
}
}
}
diff --git a/src/BirdsiteLive.sln b/src/BirdsiteLive.sln
index d600aa6..bf78d55 100644
--- a/src/BirdsiteLive.sln
+++ b/src/BirdsiteLive.sln
@@ -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}
diff --git a/src/BirdsiteLive/BirdsiteLive.csproj b/src/BirdsiteLive/BirdsiteLive.csproj
index 332831e..8b92b7c 100644
--- a/src/BirdsiteLive/BirdsiteLive.csproj
+++ b/src/BirdsiteLive/BirdsiteLive.csproj
@@ -4,6 +4,7 @@
netcoreapp3.1
d21486de-a812-47eb-a419-05682bb68856
Linux
+ 0.1.0
@@ -16,7 +17,9 @@
+
+
diff --git a/src/BirdsiteLive/Controllers/DebugingController.cs b/src/BirdsiteLive/Controllers/DebugingController.cs
index 47196c7..12ac90e 100644
--- a/src/BirdsiteLive/Controllers/DebugingController.cs
+++ b/src/BirdsiteLive/Controllers/DebugingController.cs
@@ -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 = "Woooot
",
- attachment = new string[0],
- tag = new string[0]
+ attachment = new Attachment[0],
+ tag = new Tag[0]
}
};
diff --git a/src/BirdsiteLive/Controllers/HomeController.cs b/src/BirdsiteLive/Controllers/HomeController.cs
index 7270240..ef41b65 100644
--- a/src/BirdsiteLive/Controllers/HomeController.cs
+++ b/src/BirdsiteLive/Controllers/HomeController.cs
@@ -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});
+ }
}
}
diff --git a/src/BirdsiteLive/Controllers/InboxController.cs b/src/BirdsiteLive/Controllers/InboxController.cs
index 82de7b3..a0d8748 100644
--- a/src/BirdsiteLive/Controllers/InboxController.cs
+++ b/src/BirdsiteLive/Controllers/InboxController.cs
@@ -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();
}
}
}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs
index 38f87ee..dd1b081 100644
--- a/src/BirdsiteLive/Controllers/UsersController.cs
+++ b/src/BirdsiteLive/Controllers/UsersController.cs
@@ -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 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 RequestHeaders(IHeaderDictionary header)
diff --git a/src/BirdsiteLive/Controllers/WellKnownController.cs b/src/BirdsiteLive/Controllers/WellKnownController.cs
index 613b948..9ac3bb3 100644
--- a/src/BirdsiteLive/Controllers/WellKnownController.cs
+++ b/src/BirdsiteLive/Controllers/WellKnownController.cs
@@ -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 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 links { get; set; } = new List();
- }
-
- 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; }
- }
}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/DisplayTwitterUser.cs b/src/BirdsiteLive/Models/DisplayTwitterUser.cs
new file mode 100644
index 0000000..58ba348
--- /dev/null
+++ b/src/BirdsiteLive/Models/DisplayTwitterUser.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/Link.cs b/src/BirdsiteLive/Models/WellKnownModels/Link.cs
new file mode 100644
index 0000000..e4bedfe
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/Link.cs
@@ -0,0 +1,8 @@
+namespace BirdsiteLive.Models.WellKnownModels
+{
+ public class Link
+ {
+ public string href { get; set; }
+ public string rel { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/Metadata.cs b/src/BirdsiteLive/Models/WellKnownModels/Metadata.cs
new file mode 100644
index 0000000..9f5007e
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/Metadata.cs
@@ -0,0 +1,7 @@
+namespace BirdsiteLive.Models.WellKnownModels
+{
+ public class Metadata
+ {
+ public string email { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/NodeInfoV20.cs b/src/BirdsiteLive/Models/WellKnownModels/NodeInfoV20.cs
new file mode 100644
index 0000000..032fc51
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/NodeInfoV20.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/NodeInfoV21.cs b/src/BirdsiteLive/Models/WellKnownModels/NodeInfoV21.cs
new file mode 100644
index 0000000..ce397cb
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/NodeInfoV21.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/Services.cs b/src/BirdsiteLive/Models/WellKnownModels/Services.cs
new file mode 100644
index 0000000..fa25074
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/Services.cs
@@ -0,0 +1,8 @@
+namespace BirdsiteLive.Models.WellKnownModels
+{
+ public class Services
+ {
+ public object[] inbound { get; set; }
+ public object[] outbound { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/Software.cs b/src/BirdsiteLive/Models/WellKnownModels/Software.cs
new file mode 100644
index 0000000..9cbefa6
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/Software.cs
@@ -0,0 +1,8 @@
+namespace BirdsiteLive.Models.WellKnownModels
+{
+ public class Software
+ {
+ public string name { get; set; }
+ public string version { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/SoftwareV21.cs b/src/BirdsiteLive/Models/WellKnownModels/SoftwareV21.cs
new file mode 100644
index 0000000..c6fa851
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/SoftwareV21.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/Usage.cs b/src/BirdsiteLive/Models/WellKnownModels/Usage.cs
new file mode 100644
index 0000000..693875f
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/Usage.cs
@@ -0,0 +1,8 @@
+namespace BirdsiteLive.Models.WellKnownModels
+{
+ public class Usage
+ {
+ public int localPosts { get; set; }
+ public Users users { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/Users.cs b/src/BirdsiteLive/Models/WellKnownModels/Users.cs
new file mode 100644
index 0000000..3abdb70
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/Users.cs
@@ -0,0 +1,7 @@
+namespace BirdsiteLive.Models.WellKnownModels
+{
+ public class Users
+ {
+ public int total { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/WebFingerLink.cs b/src/BirdsiteLive/Models/WellKnownModels/WebFingerLink.cs
new file mode 100644
index 0000000..9945336
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/WebFingerLink.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/WebFingerResult.cs b/src/BirdsiteLive/Models/WellKnownModels/WebFingerResult.cs
new file mode 100644
index 0000000..96c2e84
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/WebFingerResult.cs
@@ -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 links { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Models/WellKnownModels/WellKnownNodeInfo.cs b/src/BirdsiteLive/Models/WellKnownModels/WellKnownNodeInfo.cs
new file mode 100644
index 0000000..d34abe6
--- /dev/null
+++ b/src/BirdsiteLive/Models/WellKnownModels/WellKnownNodeInfo.cs
@@ -0,0 +1,7 @@
+namespace BirdsiteLive.Models.WellKnownModels
+{
+ public class WellKnownNodeInfo
+ {
+ public Link[] links { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Program.cs b/src/BirdsiteLive/Program.cs
index c109ad2..d238b02 100644
--- a/src/BirdsiteLive/Program.cs
+++ b/src/BirdsiteLive/Program.cs
@@ -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();
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddHostedService();
});
}
}
diff --git a/src/BirdsiteLive/Services/FederationService.cs b/src/BirdsiteLive/Services/FederationService.cs
new file mode 100644
index 0000000..f2c2e94
--- /dev/null
+++ b/src/BirdsiteLive/Services/FederationService.cs
@@ -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();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive/Startup.cs b/src/BirdsiteLive/Startup.cs
index 6d07aaa..c2d7cb0 100644
--- a/src/BirdsiteLive/Startup.cs
+++ b/src/BirdsiteLive/Startup.cs
@@ -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(Configuration.GetSection("Instance"));
+ //services.Configure(Configuration.GetSection("Instance"));
//services.Configure(Configuration.GetSection("Twitter"));
services.AddControllersWithViews();
@@ -48,15 +52,39 @@ namespace BirdsiteLive
var instanceSettings = Configuration.GetSection("Instance").Get();
services.For().Use(x => instanceSettings);
+ var dbSettings = Configuration.GetSection("Db").Get();
+ services.For().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().Use(x => postgresSettings);
+
+ services.For().Use().Singleton();
+ services.For().Use().Singleton();
+ services.For().Use().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();
//_.Exclude(type => type.Name.Contains("Settings"));
-
+
_.WithDefaultConventions();
_.LookForRegistries();
diff --git a/src/BirdsiteLive/Views/Debuging/Index.cshtml b/src/BirdsiteLive/Views/Debuging/Index.cshtml
index 04ea0bf..cb56c56 100644
--- a/src/BirdsiteLive/Views/Debuging/Index.cshtml
+++ b/src/BirdsiteLive/Views/Debuging/Index.cshtml
@@ -5,14 +5,14 @@
Debug
-
-