Skip to content

Commit

Permalink
Merge pull request #176 from NicolasConstant/develop
Browse files Browse the repository at this point in the history
0.21.0 PR
  • Loading branch information
NicolasConstant authored Dec 28, 2022
2 parents 4d365e2 + 36d80be commit 8897b88
Show file tree
Hide file tree
Showing 33 changed files with 1,294 additions and 91 deletions.
1 change: 1 addition & 0 deletions src/BirdsiteLive.ActivityPub/Models/ActivityDelete.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace BirdsiteLive.ActivityPub.Models
{
public class ActivityDelete : Activity
{
public string[] to { get; set; }
[JsonProperty("object")]
public object apObject { get; set; }
}
Expand Down
1 change: 1 addition & 0 deletions src/BirdsiteLive.ActivityPub/Models/Actor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class Actor
public string name { get; set; }
public string summary { get; set; }
public string url { get; set; }
public string movedTo { get; set; }
public bool manuallyApprovesFollowers { get; set; }
public string inbox { get; set; }
public bool? discoverable { get; set; } = true;
Expand Down
51 changes: 51 additions & 0 deletions src/BirdsiteLive.Domain/ActivityPubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ namespace BirdsiteLive.Domain
{
public interface IActivityPubService
{
Task<string> GetUserIdAsync(string acct);
Task<Actor> GetUser(string objectId);
Task<HttpStatusCode> PostDataAsync<T>(T data, string targetHost, string actorUrl, string inbox = null);
Task PostNewNoteActivity(Note note, string username, string noteId, string targetHost,
string targetInbox);
Task DeleteUserAsync(string username, string targetHost, string targetInbox);
}

public class WebFinger
{
public string subject { get; set; }
public string[] aliases { get; set; }
}

public class ActivityPubService : IActivityPubService
Expand All @@ -39,6 +47,24 @@ public ActivityPubService(ICryptoService cryptoService, InstanceSettings instanc
}
#endregion

public async Task<string> GetUserIdAsync(string acct)
{
var splittedAcct = acct.Trim('@').Split('@');

var url = $"https://{splittedAcct[1]}/.well-known/webfinger?resource=acct:{splittedAcct[0]}@{splittedAcct[1]}";

var httpClient = _httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var result = await httpClient.GetAsync(url);

result.EnsureSuccessStatusCode();

var content = await result.Content.ReadAsStringAsync();

var actor = JsonConvert.DeserializeObject<WebFinger>(content);
return actor.aliases.FirstOrDefault();
}

public async Task<Actor> GetUser(string objectId)
{
var httpClient = _httpClientFactory.CreateClient();
Expand All @@ -57,6 +83,31 @@ public async Task<Actor> GetUser(string objectId)
return actor;
}

public async Task DeleteUserAsync(string username, string targetHost, string targetInbox)
{
try
{
var actor = UrlFactory.GetActorUrl(_instanceSettings.Domain, username);

var deleteUser = new ActivityDelete
{
context = "https://www.w3.org/ns/activitystreams",
id = $"{actor}#delete",
type = "Delete",
actor = actor,
to = new [] { "https://www.w3.org/ns/activitystreams#Public" },
apObject = actor
};

await PostDataAsync(deleteUser, targetHost, actor, targetInbox);
}
catch (Exception e)
{
_logger.LogError(e, "Error deleting {Username} to {Host}{Inbox}", username, targetHost, targetInbox);
throw;
}
}

public async Task PostNewNoteActivity(Note note, string username, string noteId, string targetHost, string targetInbox)
{
try
Expand Down
4 changes: 4 additions & 0 deletions src/BirdsiteLive.Domain/BirdsiteLive.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
<ProjectReference Include="..\DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Enum\" />
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions src/BirdsiteLive.Domain/Enum/MigrationTypeEnum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace BirdsiteLive.Domain.Enum
{
public enum MigrationTypeEnum
{
Unknown = 0,
Migration = 1,
Deletion = 2
}
}
281 changes: 281 additions & 0 deletions src/BirdsiteLive.Domain/MigrationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
using System;
using System.Linq;
using BirdsiteLive.Twitter;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.Enum;

namespace BirdsiteLive.Domain
{
public class MigrationService
{
private readonly InstanceSettings _instanceSettings;
private readonly ITwitterTweetsService _twitterTweetsService;
private readonly IActivityPubService _activityPubService;
private readonly ITwitterUserDal _twitterUserDal;
private readonly IFollowersDal _followersDal;

#region Ctor
public MigrationService(ITwitterTweetsService twitterTweetsService, IActivityPubService activityPubService, ITwitterUserDal twitterUserDal, IFollowersDal followersDal, InstanceSettings instanceSettings)
{
_twitterTweetsService = twitterTweetsService;
_activityPubService = activityPubService;
_twitterUserDal = twitterUserDal;
_followersDal = followersDal;
_instanceSettings = instanceSettings;
}
#endregion

public string GetMigrationCode(string acct)
{
var hash = GetHashString(acct);
return $"[[BirdsiteLIVE-MigrationCode|{hash.Substring(0, 10)}]]";
}

public string GetDeletionCode(string acct)
{
var hash = GetHashString(acct);
return $"[[BirdsiteLIVE-DeletionCode|{hash.Substring(0, 10)}]]";
}

public bool ValidateTweet(string acct, string tweetId, MigrationTypeEnum type)
{
string code;
if (type == MigrationTypeEnum.Migration)
code = GetMigrationCode(acct);
else if (type == MigrationTypeEnum.Deletion)
code = GetDeletionCode(acct);
else
throw new NotImplementedException();

var castedTweetId = ExtractedTweetId(tweetId);
var tweet = _twitterTweetsService.GetTweet(castedTweetId);

if (tweet == null)
throw new Exception("Tweet not found");

if (tweet.CreatorName.Trim().ToLowerInvariant() != acct.Trim().ToLowerInvariant())
throw new Exception($"Tweet not published by @{acct}");

if (!tweet.MessageContent.Contains(code))
{
var message = "Tweet don't have migration code";
if (type == MigrationTypeEnum.Deletion)
message = "Tweet don't have deletion code";

throw new Exception(message);
}

return true;
}

private long ExtractedTweetId(string tweetId)
{
if (string.IsNullOrWhiteSpace(tweetId))
throw new ArgumentException("No provided Tweet ID");

long castedId;
if (long.TryParse(tweetId, out castedId))
return castedId;

var urlPart = tweetId.Split('/').LastOrDefault();
if (long.TryParse(urlPart, out castedId))
return castedId;

throw new ArgumentException("Unvalid Tweet ID");
}

public async Task<ValidatedFediverseUser> ValidateFediverseAcctAsync(string fediverseAcct)
{
if (string.IsNullOrWhiteSpace(fediverseAcct))
throw new ArgumentException("Please provide Fediverse account");

if (!fediverseAcct.Contains('@') || !fediverseAcct.StartsWith("@") || fediverseAcct.Trim('@').Split('@').Length != 2)
throw new ArgumentException("Please provide valid Fediverse handle");

var objectId = await _activityPubService.GetUserIdAsync(fediverseAcct);
var user = await _activityPubService.GetUser(objectId);

var result = new ValidatedFediverseUser
{
FediverseAcct = fediverseAcct,
ObjectId = objectId,
User = user,
IsValid = user != null
};

return result;
}

public async Task MigrateAccountAsync(ValidatedFediverseUser validatedUser, string acct)
{
// Apply moved to
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
if (twitterAccount == null)
{
await _twitterUserDal.CreateTwitterUserAsync(acct, -1, validatedUser.ObjectId, validatedUser.FediverseAcct);
twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
}

twitterAccount.MovedTo = validatedUser.User.id;
twitterAccount.MovedToAcct = validatedUser.FediverseAcct;
twitterAccount.LastSync = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(twitterAccount);

// Notify Followers
var message = $@"<p>[BSL MIRROR SERVICE NOTIFICATION]<br/>This bot has been disabled by its original owner.<br/>It has been redirected to {validatedUser.FediverseAcct}.</p>";
NotifyFollowers(acct, twitterAccount, message);
}

private void NotifyFollowers(string acct, SyncTwitterUser twitterAccount, string message)
{
var t = Task.Run(async () =>
{
var followers = await _followersDal.GetFollowersAsync(twitterAccount.Id);
foreach (var follower in followers)
{
try
{
var noteId = Guid.NewGuid().ToString();
var actorUrl = UrlFactory.GetActorUrl(_instanceSettings.Domain, acct);
var noteUrl = UrlFactory.GetNoteUrl(_instanceSettings.Domain, acct, noteId);

//var to = validatedUser.ObjectId;
var to = follower.ActorId;
var cc = new string[0];

var note = new Note
{
id = noteUrl,

published = DateTime.UtcNow.ToString("s") + "Z",
url = noteUrl,
attributedTo = actorUrl,

to = new[] { to },
cc = cc,

content = message,
tag = new Tag[]{
new Tag()
{
type = "Mention",
href = follower.ActorId,
name = $"@{follower.Acct}@{follower.Host}"
}
},
};

if (!string.IsNullOrWhiteSpace(follower.SharedInboxRoute))
await _activityPubService.PostNewNoteActivity(note, acct, noteId, follower.Host, follower.SharedInboxRoute);
else
await _activityPubService.PostNewNoteActivity(note, acct, noteId, follower.Host, follower.InboxRoute);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
});
}

public async Task DeleteAccountAsync(string acct)
{
// Apply deleted state
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
if (twitterAccount == null)
{
await _twitterUserDal.CreateTwitterUserAsync(acct, -1);
twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
}

twitterAccount.Deleted = true;
twitterAccount.LastSync = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(twitterAccount);

// Notify Followers
var message = $@"<p>[BSL MIRROR SERVICE NOTIFICATION]<br/>This bot has been deleted by its original owner.<br/></p>";
NotifyFollowers(acct, twitterAccount, message);

// Delete remote accounts
DeleteRemoteAccounts(acct);
}

private void DeleteRemoteAccounts(string acct)
{
var t = Task.Run(async () =>
{
var allUsers = await _followersDal.GetAllFollowersAsync();

var followersWtSharedInbox = allUsers
.Where(x => !string.IsNullOrWhiteSpace(x.SharedInboxRoute))
.GroupBy(x => x.Host)
.ToList();
foreach (var followerGroup in followersWtSharedInbox)
{
var host = followerGroup.First().Host;
var sharedInbox = followerGroup.First().SharedInboxRoute;

var t1 = Task.Run(async () =>
{
await _activityPubService.DeleteUserAsync(acct, host, sharedInbox);
});
}

var followerWtInbox = allUsers
.Where(x => !string.IsNullOrWhiteSpace(x.SharedInboxRoute))
.ToList();
foreach (var followerGroup in followerWtInbox)
{
var host = followerGroup.Host;
var sharedInbox = followerGroup.InboxRoute;

var t1 = Task.Run(async () =>
{
await _activityPubService.DeleteUserAsync(acct, host, sharedInbox);
});
}
});
}

public async Task TriggerRemoteMigrationAsync(string id, string tweetid, string handle)
{
//TODO
}

public async Task TriggerRemoteDeleteAsync(string id, string tweetid)
{
//TODO
}

private byte[] GetHash(string inputString)
{
using (HashAlgorithm algorithm = SHA256.Create())
return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString));
}

private string GetHashString(string inputString)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in GetHash(inputString))
sb.Append(b.ToString("X2"));

return sb.ToString();
}
}

public class ValidatedFediverseUser
{
public string FediverseAcct { get; set; }
public string ObjectId { get; set; }
public Actor User { get; set; }
public bool IsValid { get; set; }
}
}
Loading

0 comments on commit 8897b88

Please sign in to comment.