2024-04-05 04:26:57 +00:00
|
|
|
|
using ActivityPub.Utils;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using KristofferStrube.ActivityStreams;
|
2024-04-21 23:40:14 +00:00
|
|
|
|
|
|
|
|
|
using Object = KristofferStrube.ActivityStreams.Object;
|
2024-04-05 04:26:57 +00:00
|
|
|
|
|
|
|
|
|
namespace ActivityPub;
|
|
|
|
|
|
|
|
|
|
public enum ActivityStoreType {
|
|
|
|
|
Inbox,
|
|
|
|
|
Outbox
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class ActivityStore {
|
|
|
|
|
private static Dictionary<string, Activity> _outbox = new();
|
|
|
|
|
private static Dictionary<string, Activity> _inbox = new();
|
|
|
|
|
private Dictionary<string, Activity> _activities { get => ActivityStoreType == ActivityStoreType.Inbox ? _inbox : _outbox; }
|
|
|
|
|
private IUrlHelper Url { get; set; }
|
|
|
|
|
private ObjectStore ObjectStore { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The type (inbox or outbox) of this ActivityStore instance
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ActivityStoreType ActivityStoreType { get; private set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Default Constructor
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="url">A url helper to construct id urls</param>
|
|
|
|
|
/// <param name="activityStoreType">The typee (inbox or outbox) of this ActivityStore</param>
|
|
|
|
|
public ActivityStore(IUrlHelper url, ActivityStoreType activityStoreType) {
|
|
|
|
|
this.Url = url;
|
|
|
|
|
this.ObjectStore = new ObjectStore(url);
|
|
|
|
|
this.ActivityStoreType = activityStoreType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a new Id for an Activity
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string NewId {
|
|
|
|
|
get {
|
|
|
|
|
long idTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
|
|
while (_activities.ContainsKey(Base36.ToString(idTime))) idTime++;
|
|
|
|
|
return Base36.ToString(idTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Wraps an Object in a Create Activity
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="newObject">The object to create</param>
|
|
|
|
|
/// <returns>The create Activity, with the object inside.</returns>
|
2024-04-21 23:40:14 +00:00
|
|
|
|
public Create WrapObjectInCreate(IObject newObject) {
|
2024-04-05 04:26:57 +00:00
|
|
|
|
Create newActivity = new Create() { // new create activity
|
|
|
|
|
Actor = newObject.AttributedTo,
|
|
|
|
|
To = newObject.To,
|
|
|
|
|
Cc = newObject.Cc,
|
|
|
|
|
Bto = newObject.Bto,
|
|
|
|
|
Bcc = newObject.Bcc,
|
|
|
|
|
Audience = newObject.Audience,
|
|
|
|
|
AttributedTo = newObject.AttributedTo,
|
|
|
|
|
Name = newObject.Name?.FirstOrDefault() == null ? null : new List<string> { $"Create {newObject.Name.First()}" },
|
|
|
|
|
};
|
|
|
|
|
newActivity.Object = new List<IObjectOrLink> { newObject };
|
|
|
|
|
|
|
|
|
|
return newActivity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets an activity by Id
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="id">The Id to find</param>
|
|
|
|
|
/// <returns>The activity with the corresponding id, or null if not found</returns>
|
|
|
|
|
public Activity? GetById(string id) {
|
|
|
|
|
foreach (KeyValuePair<string, Activity> kvp in _activities) {
|
|
|
|
|
if (kvp.Key == id) return kvp.Value;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets All activities
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A lit of all activities</returns>
|
|
|
|
|
public List<Activity> GetAll() {
|
|
|
|
|
return _activities.Values.ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Inserts an activity into the data store
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="newActivity">The Activity to create</param>
|
|
|
|
|
/// <param name="runSideEffects">A boolean indicating whether side effects should be run (default true)</param>
|
|
|
|
|
/// <param name="runDelivery">A boolean indicating whether delivery tasks should be run (default true)</param>
|
|
|
|
|
/// <returns>The newly inserted activity</returns>
|
|
|
|
|
public Activity InsertActivity(Activity newActivity, bool runSideEffects = true, bool runDelivery = true) {
|
|
|
|
|
|
|
|
|
|
string id = NewId;
|
|
|
|
|
string uriId = this.Url.AbsoluteRouteUrl(ActivityStoreType == ActivityStoreType.Inbox ? "GetInboxById" : "GetOutboxById", new { id }).ToLower();
|
2024-04-21 23:40:14 +00:00
|
|
|
|
List<Uri> recipients = runDelivery ? ExtractRecipients(newActivity) : new List<Uri>();
|
|
|
|
|
newActivity.Id = uriId;
|
|
|
|
|
newActivity.Bto = newActivity.Bcc = null;
|
|
|
|
|
|
2024-04-05 04:26:57 +00:00
|
|
|
|
if (runSideEffects) newActivity = RunSideEffect(newActivity);
|
|
|
|
|
|
|
|
|
|
_activities[id] = newActivity;
|
|
|
|
|
|
|
|
|
|
if (runDelivery) RunDelivery(newActivity, recipients);
|
|
|
|
|
|
|
|
|
|
return newActivity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Extract a list of recipients for an activity.
|
|
|
|
|
/// clients must be aware that the server will only forward new Activities to addressees in the to, bto, cc, bcc, and audience fields.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="newActivity">The activity to extract recipients from</param>
|
|
|
|
|
/// <returns>A list of Uris representing inboxes to post to</returns>
|
|
|
|
|
private List<Uri> ExtractRecipients(Activity newActivity) {
|
|
|
|
|
List<Uri> recipients = new();
|
|
|
|
|
// TODO: recipients.Add(newActivity.To)
|
|
|
|
|
// TODO: recipients.Add(newActivity.Bto)
|
|
|
|
|
// TODO: recipients.Add(newActivity.Cc)
|
|
|
|
|
// TODO: recipients.Add(newActivity.Bcc)
|
|
|
|
|
// TODO: recipients.Add(newActivity.Audience)
|
|
|
|
|
// TODO: Filter out duplicates
|
|
|
|
|
// TODO: Populate lists (e.g. followers)
|
|
|
|
|
// TODO: Again, filter out duplicates
|
|
|
|
|
// TODO: change (remove?) public collection
|
|
|
|
|
return recipients;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RunDelivery(Activity newActivity, List<Uri> recipients) {
|
|
|
|
|
foreach (Uri recipient in recipients) {
|
|
|
|
|
// TODO: send a Http request to each recipient
|
|
|
|
|
// it should be a POST request with the Activity as the body
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Validates an activity
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="newActivity">the activity to validate</param>
|
|
|
|
|
/// <returns>True on successful validation</returns>
|
|
|
|
|
/// <exception cref="ArgumentException">Thown if the activity is invalid</exception>
|
|
|
|
|
public bool ValidateActivity(Activity newActivity) {
|
2024-04-21 23:40:14 +00:00
|
|
|
|
if (!(newActivity is IntransitiveActivity) && newActivity.Object?.FirstOrDefault() == null)
|
|
|
|
|
throw new ArgumentException($"'{newActivity.Type?.FirstOrDefault()}' Activities require an 'Object' property");
|
2024-04-05 04:26:57 +00:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Activity RunSideEffect(Activity newActivity) {
|
2024-04-21 23:40:14 +00:00
|
|
|
|
|
|
|
|
|
if (newActivity is IntransitiveActivity) {
|
|
|
|
|
// intransitive activities go here
|
|
|
|
|
IntransitiveActivity activity = (IntransitiveActivity)newActivity;
|
2024-04-21 23:40:36 +00:00
|
|
|
|
if (activity is Arrive) return Arrive((Arrive)activity);
|
2024-04-21 23:40:14 +00:00
|
|
|
|
else if (activity is Travel) return Travel((Travel)activity);
|
2024-04-21 23:40:36 +00:00
|
|
|
|
// else if (activity is Question) return Question((Question)activity); // TODO: Temporary until question is intransitory again
|
2024-04-21 23:40:14 +00:00
|
|
|
|
else throw new InvalidOperationException($"Activity type '{activity.Type?.FirstOrDefault()}' is unrecognized");
|
|
|
|
|
|
2024-04-05 04:26:57 +00:00
|
|
|
|
}
|
2024-04-21 23:40:14 +00:00
|
|
|
|
else {
|
|
|
|
|
Activity activity = newActivity;
|
2024-04-05 04:26:57 +00:00
|
|
|
|
|
2024-04-21 23:40:14 +00:00
|
|
|
|
if (activity is Create) return Create((Create)activity);
|
|
|
|
|
else if (activity is Update) return Update((Update)activity);
|
|
|
|
|
else if (activity is Delete) return Delete((Delete)activity);
|
|
|
|
|
else if (activity is Follow) return Follow((Follow)activity);
|
|
|
|
|
else if (activity is Add) return Add((Add)activity);
|
|
|
|
|
else if (activity is Remove) return Remove((Remove)activity);
|
|
|
|
|
else if (activity is Like) return Like((Like)activity);
|
|
|
|
|
else if (activity is Dislike) return Dislike((Dislike)activity);
|
|
|
|
|
else if (activity is Block) return Block((Block)activity);
|
|
|
|
|
else if (activity is Undo) return Undo((Undo)activity);
|
|
|
|
|
else if (activity is Accept) return Accept((Accept)activity);
|
|
|
|
|
else if (activity is Announce) return Announce((Announce)activity);
|
|
|
|
|
else if (activity is Flag) return Flag((Flag)activity);
|
|
|
|
|
else if (activity is Ignore) return Ignore((Ignore)activity);
|
|
|
|
|
else if (activity is Invite) return Invite((Invite)activity);
|
|
|
|
|
else if (activity is Join) return Join((Join)activity);
|
|
|
|
|
else if (activity is Leave) return Leave((Leave)activity);
|
|
|
|
|
else if (activity is Listen) return Listen((Listen)activity);
|
|
|
|
|
else if (activity is Move) return Move((Move)activity);
|
|
|
|
|
else if (activity is Offer) return Offer((Offer)activity);
|
|
|
|
|
else if (activity is Reject) return Reject((Reject)activity);
|
|
|
|
|
else if (activity is Read) return Read((Read)activity);
|
|
|
|
|
else if (activity is TentativeReject) return TentativeReject((TentativeReject)activity);
|
|
|
|
|
else if (activity is TentativeAccept) return TentativeAccept((TentativeAccept)activity);
|
|
|
|
|
else if (activity is View) return View((View)activity);
|
2024-04-21 23:40:36 +00:00
|
|
|
|
else if (activity is Question) return Question((Question)activity); // TODO: Temporary until question is intransitory again
|
2024-04-21 23:40:14 +00:00
|
|
|
|
else throw new InvalidOperationException($"Activity type '{activity.Type?.FirstOrDefault()}' is unrecognized");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Create Create(Create activity) {
|
|
|
|
|
Object? newObject = activity.Object?.FirstOrDefault() as Object;
|
|
|
|
|
if (newObject == null) throw new ArgumentException("'Create' Activities require an 'Object' property");
|
|
|
|
|
activity.Object = new List<IObjectOrLink> { ObjectStore.InsertObject(newObject) };
|
|
|
|
|
return activity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Update Update(Update activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Delete Delete(Delete activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Follow Follow(Follow activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Add Add(Add activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Remove Remove(Remove activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Like Like(Like activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Dislike Dislike(Dislike activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Block Block(Block activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Undo Undo(Undo activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Accept Accept(Accept activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Announce Announce(Announce activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Flag Flag(Flag activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Ignore Ignore(Ignore activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Invite Invite(Invite activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Join Join(Join activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Leave Leave(Leave activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Listen Listen(Listen activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Move Move(Move activity) {
|
|
|
|
|
throw new NotImplementedException();
|
2024-04-05 04:26:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-21 23:40:14 +00:00
|
|
|
|
private Offer Offer(Offer activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Reject Reject(Reject activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Read Read(Read activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private TentativeReject TentativeReject(TentativeReject activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private TentativeAccept TentativeAccept(TentativeAccept activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private View View(View activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Question Question(Question activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Arrive Arrive(Arrive activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Travel Travel(Travel activity) {
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
}
|
2024-04-05 04:26:57 +00:00
|
|
|
|
}
|