AP.NET/ActivityStore.cs

242 lines
8.3 KiB
C#
Raw Normal View History

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;
}
}