gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

federatingprotocol.go (20621B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package federation
     19 
     20 import (
     21 	"context"
     22 	"errors"
     23 	"net/http"
     24 	"net/url"
     25 	"strings"
     26 
     27 	"codeberg.org/gruf/go-kv"
     28 	"github.com/superseriousbusiness/activity/pub"
     29 	"github.com/superseriousbusiness/activity/streams"
     30 	"github.com/superseriousbusiness/activity/streams/vocab"
     31 	"github.com/superseriousbusiness/gotosocial/internal/ap"
     32 	"github.com/superseriousbusiness/gotosocial/internal/config"
     33 	"github.com/superseriousbusiness/gotosocial/internal/db"
     34 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
     35 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     36 	"github.com/superseriousbusiness/gotosocial/internal/log"
     37 	"github.com/superseriousbusiness/gotosocial/internal/uris"
     38 	"github.com/superseriousbusiness/gotosocial/internal/util"
     39 )
     40 
     41 type errOtherIRIBlocked struct {
     42 	account     string
     43 	domainBlock bool
     44 	iriStrs     []string
     45 }
     46 
     47 func (e errOtherIRIBlocked) Error() string {
     48 	iriStrsNice := "[" + strings.Join(e.iriStrs, ", ") + "]"
     49 	if e.domainBlock {
     50 		return "domain block exists for one or more of " + iriStrsNice
     51 	}
     52 	return "block exists between " + e.account + " and one or more of " + iriStrsNice
     53 }
     54 
     55 func newErrOtherIRIBlocked(
     56 	account string,
     57 	domainBlock bool,
     58 	otherIRIs []*url.URL,
     59 ) error {
     60 	e := errOtherIRIBlocked{
     61 		account:     account,
     62 		domainBlock: domainBlock,
     63 		iriStrs:     make([]string, 0, len(otherIRIs)),
     64 	}
     65 
     66 	for _, iri := range otherIRIs {
     67 		e.iriStrs = append(e.iriStrs, iri.String())
     68 	}
     69 
     70 	return e
     71 }
     72 
     73 /*
     74 	GO FED FEDERATING PROTOCOL INTERFACE
     75 	FederatingProtocol contains behaviors an application needs to satisfy for the
     76 	full ActivityPub S2S implementation to be supported by this library.
     77 	It is only required if the client application wants to support the server-to-
     78 	server, or federating, protocol.
     79 	It is passed to the library as a dependency injection from the client
     80 	application.
     81 */
     82 
     83 // PostInboxRequestBodyHook callback after parsing the request body for a
     84 // federated request to the Actor's inbox.
     85 //
     86 // Can be used to set contextual information based on the Activity received.
     87 //
     88 // Warning: Neither authentication nor authorization has taken place at
     89 // this time. Doing anything beyond setting contextual information is
     90 // strongly discouraged.
     91 //
     92 // If an error is returned, it is passed back to the caller of PostInbox.
     93 // In this case, the DelegateActor implementation must not write a response
     94 // to the ResponseWriter as is expected that the caller to PostInbox will
     95 // do so when handling the error.
     96 func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
     97 	// Extract any other IRIs involved in this activity.
     98 	otherIRIs := []*url.URL{}
     99 
    100 	// Get the ID of the Activity itslf.
    101 	activityID, err := pub.GetId(activity)
    102 	if err == nil {
    103 		otherIRIs = append(otherIRIs, activityID)
    104 	}
    105 
    106 	// Check if the Activity has an 'inReplyTo'.
    107 	if replyToable, ok := activity.(ap.ReplyToable); ok {
    108 		if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
    109 			otherIRIs = append(otherIRIs, inReplyToURI)
    110 		}
    111 	}
    112 
    113 	// Check for TO and CC URIs on the Activity.
    114 	if addressable, ok := activity.(ap.Addressable); ok {
    115 		otherIRIs = append(otherIRIs, ap.ExtractToURIs(addressable)...)
    116 		otherIRIs = append(otherIRIs, ap.ExtractCcURIs(addressable)...)
    117 	}
    118 
    119 	// Now perform the same checks, but for the Object(s) of the Activity.
    120 	objectProp := activity.GetActivityStreamsObject()
    121 	for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
    122 		if iter.IsIRI() {
    123 			otherIRIs = append(otherIRIs, iter.GetIRI())
    124 			continue
    125 		}
    126 
    127 		t := iter.GetType()
    128 		if t == nil {
    129 			continue
    130 		}
    131 
    132 		objectID, err := pub.GetId(t)
    133 		if err == nil {
    134 			otherIRIs = append(otherIRIs, objectID)
    135 		}
    136 
    137 		if replyToable, ok := t.(ap.ReplyToable); ok {
    138 			if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
    139 				otherIRIs = append(otherIRIs, inReplyToURI)
    140 			}
    141 		}
    142 
    143 		if addressable, ok := t.(ap.Addressable); ok {
    144 			otherIRIs = append(otherIRIs, ap.ExtractToURIs(addressable)...)
    145 			otherIRIs = append(otherIRIs, ap.ExtractCcURIs(addressable)...)
    146 		}
    147 	}
    148 
    149 	// Clean any instances of the public URI, since
    150 	// we don't care about that in this context.
    151 	otherIRIs = func(iris []*url.URL) []*url.URL {
    152 		np := make([]*url.URL, 0, len(iris))
    153 
    154 		for _, i := range iris {
    155 			if !pub.IsPublic(i.String()) {
    156 				np = append(np, i)
    157 			}
    158 		}
    159 
    160 		return np
    161 	}(otherIRIs)
    162 
    163 	// OtherIRIs will likely contain some
    164 	// duplicate entries now, so remove them.
    165 	otherIRIs = util.UniqueURIs(otherIRIs)
    166 
    167 	// Finished, set other IRIs on the context
    168 	// so they can be checked for blocks later.
    169 	ctx = gtscontext.SetOtherIRIs(ctx, otherIRIs)
    170 	return ctx, nil
    171 }
    172 
    173 // AuthenticatePostInbox delegates the authentication of a POST to an
    174 // inbox.
    175 //
    176 // If an error is returned, it is passed back to the caller of
    177 // PostInbox. In this case, the implementation must not write a
    178 // response to the ResponseWriter as is expected that the client will
    179 // do so when handling the error. The 'authenticated' is ignored.
    180 //
    181 // If no error is returned, but authentication or authorization fails,
    182 // then authenticated must be false and error nil. It is expected that
    183 // the implementation handles writing to the ResponseWriter in this
    184 // case.
    185 //
    186 // Finally, if the authentication and authorization succeeds, then
    187 // authenticated must be true and error nil. The request will continue
    188 // to be processed.
    189 func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
    190 	log.Tracef(ctx, "received request to authenticate inbox %s", r.URL.String())
    191 
    192 	// Ensure this is an inbox path, and fetch the inbox owner
    193 	// account by parsing username from `/users/{username}/inbox`.
    194 	username, err := uris.ParseInboxPath(r.URL)
    195 	if err != nil {
    196 		err = gtserror.Newf("could not parse %s as inbox path: %w", r.URL.String(), err)
    197 		return nil, false, err
    198 	}
    199 
    200 	if username == "" {
    201 		err = gtserror.New("inbox username was empty")
    202 		return nil, false, err
    203 	}
    204 
    205 	receivingAccount, err := f.db.GetAccountByUsernameDomain(ctx, username, "")
    206 	if err != nil {
    207 		err = gtserror.Newf("could not fetch receiving account %s: %w", username, err)
    208 		return nil, false, err
    209 	}
    210 
    211 	// Check who's trying to deliver to us by inspecting the http signature.
    212 	pubKeyOwner, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username)
    213 	if errWithCode != nil {
    214 		switch errWithCode.Code() {
    215 		case http.StatusUnauthorized, http.StatusForbidden, http.StatusBadRequest:
    216 			// If codes 400, 401, or 403, obey the go-fed
    217 			// interface by writing the header and bailing.
    218 			w.WriteHeader(errWithCode.Code())
    219 			return ctx, false, nil
    220 		case http.StatusGone:
    221 			// If the requesting account's key has gone
    222 			// (410) then likely inbox post was a delete.
    223 			//
    224 			// We can just write 202 and leave: we didn't
    225 			// know about the account anyway, so we can't
    226 			// do any further processing.
    227 			w.WriteHeader(http.StatusAccepted)
    228 			return ctx, false, nil
    229 		default:
    230 			// Proper error.
    231 			return ctx, false, err
    232 		}
    233 	}
    234 
    235 	// Authentication has passed, check if we need to create a
    236 	// new instance entry for the Host of the requesting account.
    237 	if _, err := f.db.GetInstance(ctx, pubKeyOwner.Host); err != nil {
    238 		if !errors.Is(err, db.ErrNoEntries) {
    239 			// There's been an actual error.
    240 			err = gtserror.Newf("error getting instance %s: %w", pubKeyOwner.Host, err)
    241 			return ctx, false, err
    242 		}
    243 
    244 		// We don't have an entry for this
    245 		// instance yet; go dereference it.
    246 		instance, err := f.GetRemoteInstance(
    247 			gtscontext.SetFastFail(ctx),
    248 			username,
    249 			&url.URL{
    250 				Scheme: pubKeyOwner.Scheme,
    251 				Host:   pubKeyOwner.Host,
    252 			},
    253 		)
    254 		if err != nil {
    255 			err = gtserror.Newf("error dereferencing instance %s: %w", pubKeyOwner.Host, err)
    256 			return nil, false, err
    257 		}
    258 
    259 		if err := f.db.Put(ctx, instance); err != nil && !errors.Is(err, db.ErrAlreadyExists) {
    260 			err = gtserror.Newf("error inserting instance entry for %s: %w", pubKeyOwner.Host, err)
    261 			return nil, false, err
    262 		}
    263 	}
    264 
    265 	// We know the public key owner URI now, so we can
    266 	// dereference the remote account (or just get it
    267 	// from the db if we already have it).
    268 	requestingAccount, _, err := f.GetAccountByURI(
    269 		gtscontext.SetFastFail(ctx),
    270 		username,
    271 		pubKeyOwner,
    272 	)
    273 	if err != nil {
    274 		if gtserror.StatusCode(err) == http.StatusGone {
    275 			// This is the same case as the http.StatusGone check above.
    276 			// It can happen here and not there because there's a race
    277 			// where the sending server starts sending account deletion
    278 			// notifications out, we start processing, the request above
    279 			// succeeds, and *then* the profile is removed and starts
    280 			// returning 410 Gone, at which point _this_ request fails.
    281 			w.WriteHeader(http.StatusAccepted)
    282 			return ctx, false, nil
    283 		}
    284 
    285 		err = gtserror.Newf("couldn't get requesting account %s: %w", pubKeyOwner, err)
    286 		return nil, false, err
    287 	}
    288 
    289 	// We have everything we need now, set the requesting
    290 	// and receiving accounts on the context for later use.
    291 	ctx = gtscontext.SetRequestingAccount(ctx, requestingAccount)
    292 	ctx = gtscontext.SetReceivingAccount(ctx, receivingAccount)
    293 	return ctx, true, nil
    294 }
    295 
    296 // Blocked should determine whether to permit a set of actors given by
    297 // their ids are able to interact with this particular end user due to
    298 // being blocked or other application-specific logic.
    299 func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
    300 	// Fetch relevant items from request context.
    301 	// These should have been set further up the flow.
    302 	receivingAccount := gtscontext.ReceivingAccount(ctx)
    303 	if receivingAccount == nil {
    304 		err := gtserror.New("couldn't determine blocks (receiving account not set on request context)")
    305 		return false, err
    306 	}
    307 
    308 	requestingAccount := gtscontext.RequestingAccount(ctx)
    309 	if requestingAccount == nil {
    310 		err := gtserror.New("couldn't determine blocks (requesting account not set on request context)")
    311 		return false, err
    312 	}
    313 
    314 	otherIRIs := gtscontext.OtherIRIs(ctx)
    315 	if otherIRIs == nil {
    316 		err := gtserror.New("couldn't determine blocks (otherIRIs not set on request context)")
    317 		return false, err
    318 	}
    319 
    320 	l := log.
    321 		WithContext(ctx).
    322 		WithFields(kv.Fields{
    323 			{"actorIRIs", actorIRIs},
    324 			{"receivingAccount", receivingAccount.URI},
    325 			{"requestingAccount", requestingAccount.URI},
    326 			{"otherIRIs", otherIRIs},
    327 		}...)
    328 	l.Trace("checking blocks")
    329 
    330 	// Start broad by checking domain-level blocks first for
    331 	// the given actor IRIs; if any of them are domain blocked
    332 	// then we can save some work.
    333 	blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs)
    334 	if err != nil {
    335 		err = gtserror.Newf("error checking domain blocks of actorIRIs: %w", err)
    336 		return false, err
    337 	}
    338 
    339 	if blocked {
    340 		l.Trace("one or more actorIRIs are domain blocked")
    341 		return blocked, nil
    342 	}
    343 
    344 	// Now user level blocks. Receiver should not block requester.
    345 	blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID)
    346 	if err != nil {
    347 		err = gtserror.Newf("db error checking block between receiver and requester: %w", err)
    348 		return false, err
    349 	}
    350 
    351 	if blocked {
    352 		l.Trace("receiving account blocks requesting account")
    353 		return blocked, nil
    354 	}
    355 
    356 	// We've established that no blocks exist between directly
    357 	// involved actors, but what about IRIs of other actors and
    358 	// objects which are tangentially involved in the activity
    359 	// (ie., replied to, boosted)?
    360 	//
    361 	// If one or more of these other IRIs is domain blocked, or
    362 	// blocked by the receiving account, this shouldn't return
    363 	// blocked=true to send a 403, since that would be rather
    364 	// silly behavior. Instead, we should indicate to the caller
    365 	// that we should stop processing the activity and just write
    366 	// 202 Accepted instead.
    367 	//
    368 	// For this, we can use the errOtherIRIBlocked type, which
    369 	// will be checked for
    370 
    371 	// Check high-level domain blocks first.
    372 	blocked, err = f.db.AreURIsBlocked(ctx, otherIRIs)
    373 	if err != nil {
    374 		err := gtserror.Newf("error checking domain block of otherIRIs: %w", err)
    375 		return false, err
    376 	}
    377 
    378 	if blocked {
    379 		err := newErrOtherIRIBlocked(receivingAccount.URI, true, otherIRIs)
    380 		l.Trace(err.Error())
    381 		return false, err
    382 	}
    383 
    384 	// For each other IRI, check whether the IRI points to an
    385 	// account or a status, and try to get (an) accountID(s)
    386 	// from it to do further checks on.
    387 	//
    388 	// We use a map for this instead of a slice in order to
    389 	// deduplicate entries and avoid doing the same check twice.
    390 	// The map value is the host of the otherIRI.
    391 	accountIDs := make(map[string]string, len(otherIRIs))
    392 	for _, iri := range otherIRIs {
    393 		// Assemble iri string just once.
    394 		iriStr := iri.String()
    395 
    396 		account, err := f.db.GetAccountByURI(
    397 			// We're on a hot path, fetch bare minimum.
    398 			gtscontext.SetBarebones(ctx),
    399 			iriStr,
    400 		)
    401 		if err != nil && !errors.Is(err, db.ErrNoEntries) {
    402 			// Real db error.
    403 			err = gtserror.Newf("db error trying to get %s as account: %w", iriStr, err)
    404 			return false, err
    405 		} else if err == nil {
    406 			// IRI is for an account.
    407 			accountIDs[account.ID] = iri.Host
    408 			continue
    409 		}
    410 
    411 		status, err := f.db.GetStatusByURI(
    412 			// We're on a hot path, fetch bare minimum.
    413 			gtscontext.SetBarebones(ctx),
    414 			iriStr,
    415 		)
    416 		if err != nil && !errors.Is(err, db.ErrNoEntries) {
    417 			// Real db error.
    418 			err = gtserror.Newf("db error trying to get %s as status: %w", iriStr, err)
    419 			return false, err
    420 		} else if err == nil {
    421 			// IRI is for a status.
    422 			accountIDs[status.AccountID] = iri.Host
    423 			continue
    424 		}
    425 	}
    426 
    427 	// Get our own host value just once outside the loop.
    428 	ourHost := config.GetHost()
    429 
    430 	for accountID, iriHost := range accountIDs {
    431 		// Receiver shouldn't block other IRI owner.
    432 		//
    433 		// This check protects against cases where someone on our
    434 		// instance is receiving a boost from someone they don't
    435 		// block, but the boost target is the status of an account
    436 		// they DO have blocked, or the boosted status mentions an
    437 		// account they have blocked. In this case, it's v. unlikely
    438 		// they care to see the boost in their timeline, so there's
    439 		// no point in us processing it.
    440 		blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, accountID)
    441 		if err != nil {
    442 			err = gtserror.Newf("db error checking block between receiver and other account: %w", err)
    443 			return false, err
    444 		}
    445 
    446 		if blocked {
    447 			l.Trace("receiving account blocks one or more otherIRIs")
    448 			err := newErrOtherIRIBlocked(receivingAccount.URI, false, otherIRIs)
    449 			return false, err
    450 		}
    451 
    452 		// If other account is from our instance (indicated by the
    453 		// host of the URI stored in the map), ensure they don't block
    454 		// the requester.
    455 		//
    456 		// This check protects against cases where one of our users
    457 		// might be mentioned by the requesting account, and therefore
    458 		// appear in otherIRIs, but the activity itself has been sent
    459 		// to a different account on our instance. In other words, two
    460 		// accounts are gossiping about + trying to tag a third account
    461 		// who has one or the other of them blocked.
    462 		if iriHost == ourHost {
    463 			blocked, err = f.db.IsBlocked(ctx, accountID, requestingAccount.ID)
    464 			if err != nil {
    465 				err = gtserror.Newf("db error checking block between other account and requester: %w", err)
    466 				return false, err
    467 			}
    468 
    469 			if blocked {
    470 				l.Trace("one or more otherIRIs belonging to us blocks requesting account")
    471 				err := newErrOtherIRIBlocked(requestingAccount.URI, false, otherIRIs)
    472 				return false, err
    473 			}
    474 		}
    475 	}
    476 
    477 	return false, nil
    478 }
    479 
    480 // FederatingCallbacks returns the application logic that handles
    481 // ActivityStreams received from federating peers.
    482 //
    483 // Note that certain types of callbacks will be 'wrapped' with default
    484 // behaviors supported natively by the library. Other callbacks
    485 // compatible with streams.TypeResolver can be specified by 'other'.
    486 //
    487 // For example, setting the 'Create' field in the
    488 // FederatingWrappedCallbacks lets an application dependency inject
    489 // additional behaviors they want to take place, including the default
    490 // behavior supplied by this library. This is guaranteed to be compliant
    491 // with the ActivityPub Social protocol.
    492 //
    493 // To override the default behavior, instead supply the function in
    494 // 'other', which does not guarantee the application will be compliant
    495 // with the ActivityPub Social Protocol.
    496 //
    497 // Applications are not expected to handle every single ActivityStreams
    498 // type and extension. The unhandled ones are passed to DefaultCallback.
    499 func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) {
    500 	wrapped = pub.FederatingWrappedCallbacks{
    501 		// OnFollow determines what action to take for this
    502 		// particular callback if a Follow Activity is handled.
    503 		//
    504 		// For our implementation, we always want to do nothing
    505 		// because we have internal logic for handling follows.
    506 		OnFollow: pub.OnFollowDoNothing,
    507 	}
    508 
    509 	// Override some default behaviors to trigger our own side effects.
    510 	other = []interface{}{
    511 		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
    512 			return f.FederatingDB().Undo(ctx, undo)
    513 		},
    514 		func(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
    515 			return f.FederatingDB().Accept(ctx, accept)
    516 		},
    517 		func(ctx context.Context, reject vocab.ActivityStreamsReject) error {
    518 			return f.FederatingDB().Reject(ctx, reject)
    519 		},
    520 		func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error {
    521 			return f.FederatingDB().Announce(ctx, announce)
    522 		},
    523 	}
    524 
    525 	return
    526 }
    527 
    528 // DefaultCallback is called for types that go-fed can deserialize but
    529 // are not handled by the application's callbacks returned in the
    530 // Callbacks method.
    531 //
    532 // Applications are not expected to handle every single ActivityStreams
    533 // type and extension, so the unhandled ones are passed to
    534 // DefaultCallback.
    535 func (f *federator) DefaultCallback(ctx context.Context, activity pub.Activity) error {
    536 	log.Debugf(ctx, "received unhandle-able activity type (%s) so ignoring it", activity.GetTypeName())
    537 	return nil
    538 }
    539 
    540 // MaxInboxForwardingRecursionDepth determines how deep to search within
    541 // an activity to determine if inbox forwarding needs to occur.
    542 //
    543 // Zero or negative numbers indicate infinite recursion.
    544 func (f *federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
    545 	// TODO
    546 	return 4
    547 }
    548 
    549 // MaxDeliveryRecursionDepth determines how deep to search within
    550 // collections owned by peers when they are targeted to receive a
    551 // delivery.
    552 //
    553 // Zero or negative numbers indicate infinite recursion.
    554 func (f *federator) MaxDeliveryRecursionDepth(ctx context.Context) int {
    555 	// TODO
    556 	return 4
    557 }
    558 
    559 // FilterForwarding allows the implementation to apply business logic
    560 // such as blocks, spam filtering, and so on to a list of potential
    561 // Collections and OrderedCollections of recipients when inbox
    562 // forwarding has been triggered.
    563 //
    564 // The activity is provided as a reference for more intelligent
    565 // logic to be used, but the implementation must not modify it.
    566 func (f *federator) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) {
    567 	// TODO
    568 	return []*url.URL{}, nil
    569 }
    570 
    571 // GetInbox returns the OrderedCollection inbox of the actor for this
    572 // context. It is up to the implementation to provide the correct
    573 // collection for the kind of authorization given in the request.
    574 //
    575 // AuthenticateGetInbox will be called prior to this.
    576 //
    577 // Always called, regardless whether the Federated Protocol or Social
    578 // API is enabled.
    579 func (f *federator) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
    580 	// IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to outboxes and inboxes through
    581 	// the CLIENT API, not through the federation API, so we just do nothing here.
    582 	return streams.NewActivityStreamsOrderedCollectionPage(), nil
    583 }