using ActivityPub.Utils;
using Microsoft.AspNetCore.Mvc;
using KristofferStrube.ActivityStreams;
using Object = KristofferStrube.ActivityStreams.Object;
namespace ActivityPub;
///
/// An enum to differentiate inbox and outbox data stores
///
public enum ActivityStoreType {
///
/// For indicating an Inbox activity store
///
Inbox,
///
/// For indicating an Outbox activity store
///
Outbox
}
///
/// A class representing the data store for activities
///
public class ActivityStore {
private static Dictionary _outbox = new();
private static Dictionary _inbox = new();
private Dictionary _activities { get => ActivityStoreType == ActivityStoreType.Inbox ? _inbox : _outbox; }
private IUrlHelper Url { get; set; }
private ObjectStore ObjectStore { get; set; }
///
/// The type (inbox or outbox) of this ActivityStore instance
///
public ActivityStoreType ActivityStoreType { get; private set; }
///
/// Default Constructor
///
/// A url helper to construct id urls
/// The typee (inbox or outbox) of this ActivityStore
public ActivityStore(IUrlHelper url, ActivityStoreType activityStoreType) {
this.Url = url;
this.ObjectStore = new ObjectStore(url);
this.ActivityStoreType = activityStoreType;
}
///
/// Gets a new Id for an Activity
///
public string NewId {
get {
long idTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
while (_activities.ContainsKey(Base36.ToString(idTime))) idTime++;
return Base36.ToString(idTime);
}
}
///
/// Wraps an Object in a Create Activity
///
/// The object to create
/// The create Activity, with the object inside.
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 { $"Create {newObject.Name.First()}" },
};
newActivity.Object = new List { newObject };
return newActivity;
}
///
/// Gets an activity by Id
///
/// The Id to find
/// The activity with the corresponding id, or null if not found
public Activity? GetById(string id) {
foreach (KeyValuePair kvp in _activities) {
if (kvp.Key == id) return kvp.Value;
}
return null;
}
///
/// Gets All activities
///
/// A lit of all activities
public List GetAll() {
return _activities.Values.ToList();
}
///
/// Inserts an activity into the data store
///
/// The Activity to create
/// A boolean indicating whether side effects should be run (default true)
/// A boolean indicating whether delivery tasks should be run (default true)
/// The newly inserted activity
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 recipients = runDelivery ? ExtractRecipients(newActivity) : new List();
newActivity.Id = uriId;
newActivity.Bto = newActivity.Bcc = null;
if (runSideEffects) newActivity = RunSideEffect(newActivity);
_activities[id] = newActivity;
if (runDelivery) RunDelivery(newActivity, recipients);
return newActivity;
}
///
/// 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.
///
/// The activity to extract recipients from
/// A list of Uris representing inboxes to post to
private List ExtractRecipients(Activity newActivity) {
List 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 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
}
}
///
/// Validates an activity
///
/// the activity to validate
/// True on successful validation
/// Thown if the activity is invalid
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 { 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();
}
}