using ActivityPub.Utils; using Microsoft.AspNetCore.Mvc; using System.ComponentModel; using KristofferStrube.ActivityStreams; using OneOf; namespace ActivityPub; using Activity = OneOf; 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(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 { $"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) { List recipients = runDelivery ? ExtractRecipients(newActivity) : new List(); 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; } ///// ///// 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(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 recipients = runDelivery ? ExtractRecipients(newActivity) : new List(); // newActivity.Bto = newActivity.Bcc = null; // _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) { 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)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; } }