package pub import ( "bytes" "context" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" ) var ( // ErrObjectRequired indicates the activity needs its object property // set. Can be returned by DelegateActor's PostInbox or PostOutbox so a // Bad Request response is set. ErrObjectRequired = errors.New("object property required on the provided activity") // ErrTargetRequired indicates the activity needs its target property // set. Can be returned by DelegateActor's PostInbox or PostOutbox so a // Bad Request response is set. ErrTargetRequired = errors.New("target property required on the provided activity") ) // activityStreamsMediaTypes contains all of the accepted ActivityStreams media // types. Generated at init time. var activityStreamsMediaTypes []string func init() { activityStreamsMediaTypes = []string{ "application/activity+json", } jsonLdType := "application/ld+json" for _, semi := range []string{";", " ;", " ; ", "; "} { for _, profile := range []string{ "profile=https://www.w3.org/ns/activitystreams", "profile=\"https://www.w3.org/ns/activitystreams\"", } { activityStreamsMediaTypes = append( activityStreamsMediaTypes, fmt.Sprintf("%s%s%s", jsonLdType, semi, profile)) } } } // headerIsActivityPubMediaType returns true if the header string contains one // of the accepted ActivityStreams media types. // // Note we don't try to build a comprehensive parser and instead accept a // tolerable amount of whitespace since the HTTP specification is ambiguous // about the format and significance of whitespace. func headerIsActivityPubMediaType(header string) bool { for _, mediaType := range activityStreamsMediaTypes { if strings.Contains(header, mediaType) { return true } } return false } const ( // The Content-Type header. contentTypeHeader = "Content-Type" // The Accept header. acceptHeader = "Accept" ) // readActivityPubReq reads ActivityPub data from an incoming request, handling body close. func readActivityPubReq(req *http.Request) (map[string]interface{}, error) { // Ensure closed when done. defer req.Body.Close() var m map[string]interface{} // Wrap body in a JSON decoder. dec := json.NewDecoder(req.Body) // Decode JSON body as "raw" AP data map. if err := dec.Decode(&m); err != nil { return nil, err } // Perform a final second decode to ensure no trailing // garbage data or second JSON value (indicates malformed). if err := dec.Decode(&struct{}{}); err != io.EOF { return nil, errors.New("trailing data after json") } return m, nil } // readActivityPubResp reads ActivityPub data from a dereference response, handling media type check and body close. func readActivityPubResp(resp *http.Response) (map[string]interface{}, error) { // Ensure closed when done. defer resp.Body.Close() // Check the incoming response media type is the expected ActivityPub content-type. if mediaType := resp.Header.Get("Content-Type"); !headerIsActivityPubMediaType(mediaType) { return nil, fmt.Errorf("%s %s resp was not ActivityPub media type: %s", resp.Request.Method, resp.Request.URL, mediaType) } var m map[string]interface{} // Wrap body in a JSON decoder. dec := json.NewDecoder(resp.Body) // Decode JSON body as "raw" AP data map. if err := dec.Decode(&m); err != nil { return nil, err } // Perform a final second decode to ensure no trailing // garbage data or second JSON value (indicates malformed). if err := dec.Decode(&struct{}{}); err != io.EOF { return nil, errors.New("trailing data after json") } return m, nil } // isActivityPubPost returns true if the request is a POST request that has the // ActivityStreams content type header func isActivityPubPost(r *http.Request) bool { return r.Method == "POST" && headerIsActivityPubMediaType(r.Header.Get(contentTypeHeader)) } // isActivityPubGet returns true if the request is a GET request that has the // ActivityStreams content type header func isActivityPubGet(r *http.Request) bool { return r.Method == "GET" && headerIsActivityPubMediaType(r.Header.Get(acceptHeader)) } // dedupeOrderedItems deduplicates the 'orderedItems' within an ordered // collection type. Deduplication happens by the 'id' property. func dedupeOrderedItems(oc orderedItemser) error { oi := oc.GetActivityStreamsOrderedItems() if oi == nil { return nil } seen := make(map[string]bool, oi.Len()) for i := 0; i < oi.Len(); { var id *url.URL iter := oi.At(i) asType := iter.GetType() if asType != nil { var err error id, err = GetId(asType) if err != nil { return err } } else if iter.IsIRI() { id = iter.GetIRI() } else { return fmt.Errorf("element %d in OrderedCollection does not have an ID nor is an IRI", i) } if seen[id.String()] { oi.Remove(i) } else { seen[id.String()] = true i++ } } return nil } const ( // The Location header locationHeader = "Location" // Contains the ActivityStreams Content-Type value. contentTypeHeaderValue = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" // The Date header. dateHeader = "Date" // The Digest header. digestHeader = "Digest" // The delimiter used in the Digest header. digestDelimiter = "=" // SHA-256 string for the Digest header. sha256Digest = "SHA-256" ) // addResponseHeaders sets headers needed in the HTTP response, such but not // limited to the Content-Type, Date, and Digest headers. func addResponseHeaders(h http.Header, c Clock, responseContent []byte) { h.Set(contentTypeHeader, contentTypeHeaderValue) // RFC 7231 ยง7.1.1.2 h.Set(dateHeader, c.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") // RFC 3230 and RFC 5843 var b bytes.Buffer b.WriteString(sha256Digest) b.WriteString(digestDelimiter) hashed := sha256.Sum256(responseContent) b.WriteString(base64.StdEncoding.EncodeToString(hashed[:])) h.Set(digestHeader, b.String()) } // IdProperty is a property that can readily have its id obtained type IdProperty interface { // GetIRI returns the IRI of this property. When IsIRI returns false, // GetIRI will return an arbitrary value. GetIRI() *url.URL // GetType returns the value in this property as a Type. Returns nil if // the value is not an ActivityStreams type, such as an IRI or another // value. GetType() vocab.Type // IsIRI returns true if this property is an IRI. IsIRI() bool } // ToId returns an IdProperty's id. func ToId(i IdProperty) (*url.URL, error) { if i.GetType() != nil { return GetId(i.GetType()) } else if i.IsIRI() { return i.GetIRI(), nil } return nil, fmt.Errorf("cannot determine id of activitystreams property") } // GetId will attempt to find the 'id' property or, if it happens to be a // Link or derived from Link type, the 'href' property instead. // // Returns an error if the id is not set and either the 'href' property is not // valid on this type, or it is also not set. func GetId(t vocab.Type) (*url.URL, error) { if id := t.GetJSONLDId(); id != nil { return id.Get(), nil } else if h, ok := t.(hrefer); ok { if href := h.GetActivityStreamsHref(); href != nil { return href.Get(), nil } } return nil, fmt.Errorf("cannot determine id of activitystreams value") } // getInboxForwardingValues obtains the 'inReplyTo', 'object', 'target', and // 'tag' values on an ActivityStreams value. func getInboxForwardingValues(o vocab.Type) (t []vocab.Type, iri []*url.URL) { // 'inReplyTo' if i, ok := o.(inReplyToer); ok { if irt := i.GetActivityStreamsInReplyTo(); irt != nil { for iter := irt.Begin(); iter != irt.End(); iter = iter.Next() { if tv := iter.GetType(); tv != nil { t = append(t, tv) } else { iri = append(iri, iter.GetIRI()) } } } } // 'tag' if i, ok := o.(tagger); ok { if tag := i.GetActivityStreamsTag(); tag != nil { for iter := tag.Begin(); iter != tag.End(); iter = iter.Next() { if tv := iter.GetType(); tv != nil { t = append(t, tv) } else { iri = append(iri, iter.GetIRI()) } } } } // 'object' if i, ok := o.(objecter); ok { if obj := i.GetActivityStreamsObject(); obj != nil { for iter := obj.Begin(); iter != obj.End(); iter = iter.Next() { if tv := iter.GetType(); tv != nil { t = append(t, tv) } else { iri = append(iri, iter.GetIRI()) } } } } // 'target' if i, ok := o.(targeter); ok { if tar := i.GetActivityStreamsTarget(); tar != nil { for iter := tar.Begin(); iter != tar.End(); iter = iter.Next() { if tv := iter.GetType(); tv != nil { t = append(t, tv) } else { iri = append(iri, iter.GetIRI()) } } } } return } // wrapInCreate will automatically wrap the provided object in a Create // activity. This will copy over the 'to', 'bto', 'cc', 'bcc', and 'audience' // properties. It will also copy over the published time if present. func wrapInCreate(ctx context.Context, o vocab.Type, actor *url.URL) (c vocab.ActivityStreamsCreate, err error) { c = streams.NewActivityStreamsCreate() // Object property oProp := streams.NewActivityStreamsObjectProperty() oProp.AppendType(o) c.SetActivityStreamsObject(oProp) // Actor Property actorProp := streams.NewActivityStreamsActorProperty() actorProp.AppendIRI(actor) c.SetActivityStreamsActor(actorProp) // Published Property if v, ok := o.(publisheder); ok { c.SetActivityStreamsPublished(v.GetActivityStreamsPublished()) } // Copying over properties. if v, ok := o.(toer); ok { if to := v.GetActivityStreamsTo(); to != nil { activityTo := streams.NewActivityStreamsToProperty() for iter := to.Begin(); iter != to.End(); iter = iter.Next() { var id *url.URL id, err = ToId(iter) if err != nil { return } activityTo.AppendIRI(id) } c.SetActivityStreamsTo(activityTo) } } if v, ok := o.(btoer); ok { if bto := v.GetActivityStreamsBto(); bto != nil { activityBto := streams.NewActivityStreamsBtoProperty() for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() { var id *url.URL id, err = ToId(iter) if err != nil { return } activityBto.AppendIRI(id) } c.SetActivityStreamsBto(activityBto) } } if v, ok := o.(ccer); ok { if cc := v.GetActivityStreamsCc(); cc != nil { activityCc := streams.NewActivityStreamsCcProperty() for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() { var id *url.URL id, err = ToId(iter) if err != nil { return } activityCc.AppendIRI(id) } c.SetActivityStreamsCc(activityCc) } } if v, ok := o.(bccer); ok { if bcc := v.GetActivityStreamsBcc(); bcc != nil { activityBcc := streams.NewActivityStreamsBccProperty() for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() { var id *url.URL id, err = ToId(iter) if err != nil { return } activityBcc.AppendIRI(id) } c.SetActivityStreamsBcc(activityBcc) } } if v, ok := o.(audiencer); ok { if aud := v.GetActivityStreamsAudience(); aud != nil { activityAudience := streams.NewActivityStreamsAudienceProperty() for iter := aud.Begin(); iter != aud.End(); iter = iter.Next() { var id *url.URL id, err = ToId(iter) if err != nil { return } activityAudience.AppendIRI(id) } c.SetActivityStreamsAudience(activityAudience) } } return } // filterURLs removes urls whose strings match the provided filter func filterURLs(u []*url.URL, fn func(s string) bool) []*url.URL { i := 0 for i < len(u) { if fn(u[i].String()) { u = append(u[:i], u[i+1:]...) } else { i++ } } return u } const ( // PublicActivityPubIRI is the IRI that indicates an Activity is meant // to be visible for general public consumption. PublicActivityPubIRI = "https://www.w3.org/ns/activitystreams#Public" publicJsonLD = "Public" publicJsonLDAS = "as:Public" ) // IsPublic determines if an IRI string is the Public collection as defined in // the spec, including JSON-LD compliant collections. func IsPublic(s string) bool { return s == PublicActivityPubIRI || s == publicJsonLD || s == publicJsonLDAS } // getInboxes extracts the 'inbox' IRIs from actor types. func getInboxes(t []vocab.Type) (u []*url.URL, err error) { for _, elem := range t { var iri *url.URL iri, err = getInbox(elem) if err != nil { return } u = append(u, iri) } return } // getInbox extracts the 'inbox' IRI from an actor type. func getInbox(t vocab.Type) (u *url.URL, err error) { ib, ok := t.(inboxer) if !ok { err = fmt.Errorf("actor type %T has no inbox", t) return } inbox := ib.GetActivityStreamsInbox() return ToId(inbox) } // dedupeIRIs will deduplicate final inbox IRIs. The ignore list is applied to // the final list. func dedupeIRIs(recipients, ignored []*url.URL) (out []*url.URL) { ignoredMap := make(map[string]bool, len(ignored)) for _, elem := range ignored { ignoredMap[elem.String()] = true } outMap := make(map[string]bool, len(recipients)) for _, k := range recipients { kStr := k.String() if !ignoredMap[kStr] && !outMap[kStr] { out = append(out, k) outMap[kStr] = true } } return } // removeOne removes any occurrences of entry from a slice of entries. func removeOne(entries []*url.URL, entry *url.URL) (out []*url.URL) { for _, e := range entries { if e.String() != entry.String() { out = append(out, e) } } return out } // stripHiddenRecipients removes "bto" and "bcc" from the activity. // // Note that this requirement of the specification is under "Section 6: Client // to Server Interactions", the Social API, and not the Federative API. func stripHiddenRecipients(activity Activity) { activity.SetActivityStreamsBto(nil) activity.SetActivityStreamsBcc(nil) op := activity.GetActivityStreamsObject() if op != nil { for iter := op.Begin(); iter != op.End(); iter = iter.Next() { if v, ok := iter.GetType().(btoer); ok { v.SetActivityStreamsBto(nil) } if v, ok := iter.GetType().(bccer); ok { v.SetActivityStreamsBcc(nil) } } } } // mustHaveActivityOriginMatchObjects ensures that the Host in the activity id // IRI matches all of the Hosts in the object id IRIs. func mustHaveActivityOriginMatchObjects(a Activity) error { originIRI, err := GetId(a) if err != nil { return err } originHost := originIRI.Host op := a.GetActivityStreamsObject() if op == nil || op.Len() == 0 { return nil } for iter := op.Begin(); iter != op.End(); iter = iter.Next() { iri, err := ToId(iter) if err != nil { return err } if originHost != iri.Host { return fmt.Errorf("object %q: not in activity origin", iri) } } return nil } // normalizeRecipients ensures the activity and object have the same 'to', // 'bto', 'cc', 'bcc', and 'audience' properties. Copy the Activity's recipients // to objects, and the objects to the activity, but does NOT copy objects' // recipients to each other. func normalizeRecipients(a vocab.ActivityStreamsCreate) error { // Phase 0: Acquire all recipients on the activity. // // Obtain the actorTo map actorToMap := make(map[string]*url.URL) actorTo := a.GetActivityStreamsTo() if actorTo == nil { actorTo = streams.NewActivityStreamsToProperty() a.SetActivityStreamsTo(actorTo) } for iter := actorTo.Begin(); iter != actorTo.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } actorToMap[id.String()] = id } // Obtain the actorBto map actorBtoMap := make(map[string]*url.URL) actorBto := a.GetActivityStreamsBto() if actorBto == nil { actorBto = streams.NewActivityStreamsBtoProperty() a.SetActivityStreamsBto(actorBto) } for iter := actorBto.Begin(); iter != actorBto.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } actorBtoMap[id.String()] = id } // Obtain the actorCc map actorCcMap := make(map[string]*url.URL) actorCc := a.GetActivityStreamsCc() if actorCc == nil { actorCc = streams.NewActivityStreamsCcProperty() a.SetActivityStreamsCc(actorCc) } for iter := actorCc.Begin(); iter != actorCc.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } actorCcMap[id.String()] = id } // Obtain the actorBcc map actorBccMap := make(map[string]*url.URL) actorBcc := a.GetActivityStreamsBcc() if actorBcc == nil { actorBcc = streams.NewActivityStreamsBccProperty() a.SetActivityStreamsBcc(actorBcc) } for iter := actorBcc.Begin(); iter != actorBcc.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } actorBccMap[id.String()] = id } // Obtain the actorAudience map actorAudienceMap := make(map[string]*url.URL) actorAudience := a.GetActivityStreamsAudience() if actorAudience == nil { actorAudience = streams.NewActivityStreamsAudienceProperty() a.SetActivityStreamsAudience(actorAudience) } for iter := actorAudience.Begin(); iter != actorAudience.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } actorAudienceMap[id.String()] = id } // Obtain the objects maps for each recipient type. o := a.GetActivityStreamsObject() objsTo := make([]map[string]*url.URL, o.Len()) objsBto := make([]map[string]*url.URL, o.Len()) objsCc := make([]map[string]*url.URL, o.Len()) objsBcc := make([]map[string]*url.URL, o.Len()) objsAudience := make([]map[string]*url.URL, o.Len()) for i := 0; i < o.Len(); i++ { iter := o.At(i) // Phase 1: Acquire all existing recipients on the object. // // Object to objsTo[i] = make(map[string]*url.URL) var oTo vocab.ActivityStreamsToProperty if tr, ok := iter.GetType().(toer); !ok { return fmt.Errorf("the Create object at %d has no 'to' property", i) } else { oTo = tr.GetActivityStreamsTo() if oTo == nil { oTo = streams.NewActivityStreamsToProperty() tr.SetActivityStreamsTo(oTo) } } for iter := oTo.Begin(); iter != oTo.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } objsTo[i][id.String()] = id } // Object bto objsBto[i] = make(map[string]*url.URL) var oBto vocab.ActivityStreamsBtoProperty if tr, ok := iter.GetType().(btoer); !ok { return fmt.Errorf("the Create object at %d has no 'bto' property", i) } else { oBto = tr.GetActivityStreamsBto() if oBto == nil { oBto = streams.NewActivityStreamsBtoProperty() tr.SetActivityStreamsBto(oBto) } } for iter := oBto.Begin(); iter != oBto.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } objsBto[i][id.String()] = id } // Object cc objsCc[i] = make(map[string]*url.URL) var oCc vocab.ActivityStreamsCcProperty if tr, ok := iter.GetType().(ccer); !ok { return fmt.Errorf("the Create object at %d has no 'cc' property", i) } else { oCc = tr.GetActivityStreamsCc() if oCc == nil { oCc = streams.NewActivityStreamsCcProperty() tr.SetActivityStreamsCc(oCc) } } for iter := oCc.Begin(); iter != oCc.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } objsCc[i][id.String()] = id } // Object bcc objsBcc[i] = make(map[string]*url.URL) var oBcc vocab.ActivityStreamsBccProperty if tr, ok := iter.GetType().(bccer); !ok { return fmt.Errorf("the Create object at %d has no 'bcc' property", i) } else { oBcc = tr.GetActivityStreamsBcc() if oBcc == nil { oBcc = streams.NewActivityStreamsBccProperty() tr.SetActivityStreamsBcc(oBcc) } } for iter := oBcc.Begin(); iter != oBcc.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } objsBcc[i][id.String()] = id } // Object audience objsAudience[i] = make(map[string]*url.URL) var oAudience vocab.ActivityStreamsAudienceProperty if tr, ok := iter.GetType().(audiencer); !ok { return fmt.Errorf("the Create object at %d has no 'audience' property", i) } else { oAudience = tr.GetActivityStreamsAudience() if oAudience == nil { oAudience = streams.NewActivityStreamsAudienceProperty() tr.SetActivityStreamsAudience(oAudience) } } for iter := oAudience.Begin(); iter != oAudience.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } objsAudience[i][id.String()] = id } // Phase 2: Apply missing recipients to the object from the // activity. // // Activity to -> Object to for k, v := range actorToMap { if _, ok := objsTo[i][k]; !ok { oTo.AppendIRI(v) } } // Activity bto -> Object bto for k, v := range actorBtoMap { if _, ok := objsBto[i][k]; !ok { oBto.AppendIRI(v) } } // Activity cc -> Object cc for k, v := range actorCcMap { if _, ok := objsCc[i][k]; !ok { oCc.AppendIRI(v) } } // Activity bcc -> Object bcc for k, v := range actorBccMap { if _, ok := objsBcc[i][k]; !ok { oBcc.AppendIRI(v) } } // Activity audience -> Object audience for k, v := range actorAudienceMap { if _, ok := objsAudience[i][k]; !ok { oAudience.AppendIRI(v) } } } // Phase 3: Apply missing recipients to the activity from the objects. // // Object to -> Activity to for i := 0; i < len(objsTo); i++ { for k, v := range objsTo[i] { if _, ok := actorToMap[k]; !ok { actorTo.AppendIRI(v) } } } // Object bto -> Activity bto for i := 0; i < len(objsBto); i++ { for k, v := range objsBto[i] { if _, ok := actorBtoMap[k]; !ok { actorBto.AppendIRI(v) } } } // Object cc -> Activity cc for i := 0; i < len(objsCc); i++ { for k, v := range objsCc[i] { if _, ok := actorCcMap[k]; !ok { actorCc.AppendIRI(v) } } } // Object bcc -> Activity bcc for i := 0; i < len(objsBcc); i++ { for k, v := range objsBcc[i] { if _, ok := actorBccMap[k]; !ok { actorBcc.AppendIRI(v) } } } // Object audience -> Activity audience for i := 0; i < len(objsAudience); i++ { for k, v := range objsAudience[i] { if _, ok := actorAudienceMap[k]; !ok { actorAudience.AppendIRI(v) } } } return nil } // toTombstone creates a Tombstone object for the given ActivityStreams value. func toTombstone(obj vocab.Type, id *url.URL, now time.Time) vocab.ActivityStreamsTombstone { tomb := streams.NewActivityStreamsTombstone() // id property idProp := streams.NewJSONLDIdProperty() idProp.Set(id) tomb.SetJSONLDId(idProp) // formerType property former := streams.NewActivityStreamsFormerTypeProperty() tomb.SetActivityStreamsFormerType(former) // Populate Former Type former.AppendXMLSchemaString(obj.GetTypeName()) // Copy over the published property if it existed if pubber, ok := obj.(publisheder); ok { if pub := pubber.GetActivityStreamsPublished(); pub != nil { tomb.SetActivityStreamsPublished(pub) } } // Copy over the updated property if it existed if upder, ok := obj.(updateder); ok { if upd := upder.GetActivityStreamsUpdated(); upd != nil { tomb.SetActivityStreamsUpdated(upd) } } // Set deleted time to now. deleted := streams.NewActivityStreamsDeletedProperty() deleted.Set(now) tomb.SetActivityStreamsDeleted(deleted) return tomb } // mustHaveActivityActorsMatchObjectActors ensures that the actors on types in // the 'object' property are all listed in the 'actor' property. func mustHaveActivityActorsMatchObjectActors(c context.Context, actors vocab.ActivityStreamsActorProperty, op vocab.ActivityStreamsObjectProperty, newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error), boxIRI *url.URL, ) error { activityActorMap := make(map[string]bool, actors.Len()) for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } activityActorMap[id.String()] = true } for iter := op.Begin(); iter != op.End(); iter = iter.Next() { iri, err := ToId(iter) if err != nil { return err } // Attempt to dereference the IRI, regardless whether it is a // type or IRI tport, err := newTransport(c, boxIRI, goFedUserAgent()) if err != nil { return err } resp, err := tport.Dereference(c, iri) if err != nil { return err } m, err := readActivityPubResp(resp) if err != nil { return err } t, err := streams.ToType(c, m) if err != nil { return err } ac, ok := t.(actorer) if !ok { return fmt.Errorf("cannot verify actors: object value has no 'actor' property") } objActors := ac.GetActivityStreamsActor() for iter := objActors.Begin(); iter != objActors.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } if !activityActorMap[id.String()] { return fmt.Errorf("activity does not have all actors from its object's actors") } } } return nil } // add implements the logic of adding object ids to a target Collection or // OrderedCollection. This logic is shared by both the C2S and S2S protocols. func add(c context.Context, op vocab.ActivityStreamsObjectProperty, target vocab.ActivityStreamsTargetProperty, db Database, ) error { opIds := make([]*url.URL, 0, op.Len()) for iter := op.Begin(); iter != op.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } opIds = append(opIds, id) } targetIds := make([]*url.URL, 0, op.Len()) for iter := target.Begin(); iter != target.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } targetIds = append(targetIds, id) } // Create anonymous loop function to be able to properly scope the defer // for the database lock at each iteration. loopFn := func(t *url.URL) error { unlock, err := db.Lock(c, t) if err != nil { return err } defer unlock() if owns, err := db.Owns(c, t); err != nil { return err } else if !owns { return nil } tp, err := db.Get(c, t) if err != nil { return err } if streams.IsOrExtendsActivityStreamsOrderedCollection(tp) { oi, ok := tp.(orderedItemser) if !ok { return fmt.Errorf("type extending from OrderedCollection cannot convert to orderedItemser interface") } oiProp := oi.GetActivityStreamsOrderedItems() if oiProp == nil { oiProp = streams.NewActivityStreamsOrderedItemsProperty() oi.SetActivityStreamsOrderedItems(oiProp) } for _, objId := range opIds { oiProp.AppendIRI(objId) } } else if streams.IsOrExtendsActivityStreamsCollection(tp) { i, ok := tp.(itemser) if !ok { return fmt.Errorf("type extending from Collection cannot convert to itemser interface") } iProp := i.GetActivityStreamsItems() if iProp == nil { iProp = streams.NewActivityStreamsItemsProperty() i.SetActivityStreamsItems(iProp) } for _, objId := range opIds { iProp.AppendIRI(objId) } } else { return fmt.Errorf("target in Add is neither a Collection nor an OrderedCollection") } err = db.Update(c, tp) if err != nil { return err } return nil } for _, t := range targetIds { if err := loopFn(t); err != nil { return err } } return nil } // remove implements the logic of removing object ids to a target Collection or // OrderedCollection. This logic is shared by both the C2S and S2S protocols. func remove(c context.Context, op vocab.ActivityStreamsObjectProperty, target vocab.ActivityStreamsTargetProperty, db Database, ) error { opIds := make(map[string]bool, op.Len()) for iter := op.Begin(); iter != op.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } opIds[id.String()] = true } targetIds := make([]*url.URL, 0, op.Len()) for iter := target.Begin(); iter != target.End(); iter = iter.Next() { id, err := ToId(iter) if err != nil { return err } targetIds = append(targetIds, id) } // Create anonymous loop function to be able to properly scope the defer // for the database lock at each iteration. loopFn := func(t *url.URL) error { unlock, err := db.Lock(c, t) if err != nil { return err } defer unlock() if owns, err := db.Owns(c, t); err != nil { return err } else if !owns { return nil } tp, err := db.Get(c, t) if err != nil { return err } if streams.IsOrExtendsActivityStreamsOrderedCollection(tp) { oi, ok := tp.(orderedItemser) if !ok { return fmt.Errorf("type extending from OrderedCollection cannot convert to orderedItemser interface") } oiProp := oi.GetActivityStreamsOrderedItems() if oiProp != nil { for i := 0; i < oiProp.Len(); /*Conditional*/ { id, err := ToId(oiProp.At(i)) if err != nil { return err } if opIds[id.String()] { oiProp.Remove(i) } else { i++ } } } } else if streams.IsOrExtendsActivityStreamsCollection(tp) { i, ok := tp.(itemser) if !ok { return fmt.Errorf("type extending from Collection cannot convert to itemser interface") } iProp := i.GetActivityStreamsItems() if iProp != nil { for i := 0; i < iProp.Len(); /*Conditional*/ { id, err := ToId(iProp.At(i)) if err != nil { return err } if opIds[id.String()] { iProp.Remove(i) } else { i++ } } } } else { return fmt.Errorf("target in Remove is neither a Collection nor an OrderedCollection") } err = db.Update(c, tp) if err != nil { return err } return nil } for _, t := range targetIds { if err := loopFn(t); err != nil { return err } } return nil } // clearSensitiveFields removes the 'bto' and 'bcc' entries on the given value // and recursively on every 'object' property value. func clearSensitiveFields(obj vocab.Type) { if t, ok := obj.(btoer); ok { t.SetActivityStreamsBto(nil) } if t, ok := obj.(bccer); ok { t.SetActivityStreamsBcc(nil) } if t, ok := obj.(objecter); ok { op := t.GetActivityStreamsObject() if op != nil { for iter := op.Begin(); iter != op.End(); iter = iter.Next() { clearSensitiveFields(iter.GetType()) } } } } // requestId forms an ActivityPub id based on the HTTP request. Always assumes // that the id is HTTPS. func requestId(r *http.Request, scheme string) *url.URL { id := r.URL id.Host = r.Host id.Scheme = scheme return id }