AP.NET/ActivityStore.cs

321 lines
11 KiB
C#

using ActivityPub.Utils;
using Microsoft.AspNetCore.Mvc;
using KristofferStrube.ActivityStreams;
using Object = KristofferStrube.ActivityStreams.Object;
namespace ActivityPub;
/// <summary>
/// An enum to differentiate inbox and outbox data stores
/// </summary>
public enum ActivityStoreType {
/// <summary>
/// For indicating an Inbox activity store
/// </summary>
Inbox,
/// <summary>
/// For indicating an Outbox activity store
/// </summary>
Outbox
}
/// <summary>
/// A class representing the data store for activities
/// </summary>
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(IObject 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) {
string id = NewId;
string? uriId = Url.AbsoluteRouteUrl(ActivityStoreType == ActivityStoreType.Inbox ? "GetInboxById" : "GetOutboxById", new { id })?.ToLower();
List<Uri> recipients = runDelivery ? ExtractRecipients(newActivity) : new List<Uri>();
newActivity.Id = uriId;
newActivity.Bto = newActivity.Bcc = null;
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) {
if (!(newActivity is IntransitiveActivity) && newActivity.Object?.FirstOrDefault() == null)
throw new ArgumentException($"'{newActivity.Type?.FirstOrDefault()}' Activities require an 'Object' property");
return true;
}
private Activity RunSideEffect(Activity newActivity) {
if (newActivity is IntransitiveActivity) {
// intransitive activities go here
IntransitiveActivity activity = (IntransitiveActivity)newActivity;
if (activity is Arrive) return Arrive((Arrive)activity);
else if (activity is Travel) return Travel((Travel)activity);
else if (activity is Question) return Question((Question)activity);
else throw new InvalidOperationException($"Activity type '{activity.Type?.FirstOrDefault()}' is unrecognized");
}
else {
Activity activity = newActivity;
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);
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();
}
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();
}
}