using ActivityPub.Utils; using Microsoft.AspNetCore.Mvc; using KristofferStrube.ActivityStreams; using Object = KristofferStrube.ActivityStreams.Object; namespace ActivityPub; public enum ActivityStoreType { Inbox, Outbox } 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 = this.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); // TODO: Temporary until question is intransitory again 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 if (activity is Question) return Question((Question)activity); // TODO: Temporary until question is intransitory again 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(); } }