242 lines
8.3 KiB
C#
242 lines
8.3 KiB
C#
|
using ActivityPub.Utils;
|
|||
|
using Microsoft.AspNetCore.Mvc;
|
|||
|
using System.ComponentModel;
|
|||
|
using KristofferStrube.ActivityStreams;
|
|||
|
using OneOf;
|
|||
|
|
|||
|
namespace ActivityPub;
|
|||
|
using Activity = OneOf<Activity, IntransitiveActiviy>;
|
|||
|
|
|||
|
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>
|
|||
|
public Create WrapObjectInCreate(KristofferStrube.ActivityStreams.Object newObject) {
|
|||
|
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) {
|
|||
|
List<Uri> recipients = runDelivery ? ExtractRecipients(newActivity) : new List<Uri>();
|
|||
|
|
|||
|
string id = NewId;
|
|||
|
string uriId = this.Url.AbsoluteRouteUrl(ActivityStoreType == ActivityStoreType.Inbox ? "GetInboxById" : "GetOutboxById", new { id }).ToLower();
|
|||
|
newActivity.Switch(
|
|||
|
_ => { _.Id = uriId; _.Bto = _.Bcc = null; },
|
|||
|
_ => { _.Id = uriId; _.Bto = _.Bcc = null; }
|
|||
|
);
|
|||
|
if (runSideEffects) newActivity = RunSideEffect(newActivity);
|
|||
|
|
|||
|
_activities[id] = newActivity;
|
|||
|
|
|||
|
if (runDelivery) RunDelivery(newActivity, recipients);
|
|||
|
|
|||
|
return newActivity;
|
|||
|
}
|
|||
|
|
|||
|
///// <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(IntransitiveActiviy newActivity, bool runSideEffects = true, bool runDelivery = true) {
|
|||
|
// string id = NewId;
|
|||
|
// newActivity.Id = this.Url.AbsoluteRouteUrl(ActivityStoreType == ActivityStoreType.Inbox ? "GetInboxById" : "GetOutboxById", new { id }).ToLower();
|
|||
|
// if (runSideEffects) newActivity = RunSideEffect(newActivity);
|
|||
|
|
|||
|
// List<Uri> recipients = runDelivery ? ExtractRecipients(newActivity) : new List<Uri>();
|
|||
|
// newActivity.Bto = newActivity.Bcc = null;
|
|||
|
|
|||
|
// _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) {
|
|||
|
string? type = newActivity.Match(
|
|||
|
_ => _.Type?.FirstOrDefault(),
|
|||
|
_ => _.Type?.FirstOrDefault()
|
|||
|
);
|
|||
|
switch (type) {
|
|||
|
case "Create":
|
|||
|
case "Update":
|
|||
|
case "Delete":
|
|||
|
case "Follow":
|
|||
|
case "Add":
|
|||
|
case "Remove":
|
|||
|
case "Like":
|
|||
|
case "Dislike":
|
|||
|
case "Block":
|
|||
|
|
|||
|
if (!newActivity.IsT0 || newActivity.AsT0.Object == null) throw new ArgumentException($"'{type}' Activities require an 'Object' property");
|
|||
|
break;
|
|||
|
default: break;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
private Activity RunSideEffect(Activity newActivity) {
|
|||
|
string? type = newActivity.Match(
|
|||
|
_ => _.Type?.FirstOrDefault(),
|
|||
|
_ => _.Type?.FirstOrDefault()
|
|||
|
);
|
|||
|
switch (type) {
|
|||
|
case "Create":
|
|||
|
KristofferStrube.ActivityStreams.Object? newObject = newActivity.AsT0.Object.FirstOrDefault() as KristofferStrube.ActivityStreams.Object;
|
|||
|
if (newObject == null) throw new ArgumentException("'Create' Activities require an 'Object' property");
|
|||
|
newActivity.AsT0.Object = new List<IObjectOrLink> { (IObjectOrLink)ObjectStore.InsertObject(newObject) };
|
|||
|
break;
|
|||
|
|
|||
|
case "Update":
|
|||
|
case "Delete":
|
|||
|
case "Follow":
|
|||
|
case "Add":
|
|||
|
case "Remove":
|
|||
|
case "Like":
|
|||
|
case "Dislike":
|
|||
|
case "Block":
|
|||
|
case "Undo":
|
|||
|
|
|||
|
case "Accept":
|
|||
|
case "Announce":
|
|||
|
case "Arrive":
|
|||
|
case "Flag":
|
|||
|
case "Ignore":
|
|||
|
case "Invite":
|
|||
|
case "Join":
|
|||
|
case "Leave":
|
|||
|
case "Listen":
|
|||
|
case "Move":
|
|||
|
case "Offer":
|
|||
|
case "Question":
|
|||
|
case "Reject":
|
|||
|
case "Read":
|
|||
|
case "TentativeReject":
|
|||
|
case "TentativeAccept":
|
|||
|
case "Travel":
|
|||
|
case "View":
|
|||
|
throw new NotImplementedException();
|
|||
|
default:
|
|||
|
throw new InvalidEnumArgumentException($"Invalid Activity Type '{type}'");
|
|||
|
}
|
|||
|
|
|||
|
return newActivity;
|
|||
|
}
|
|||
|
|
|||
|
}
|