forked from mirrors/gotosocial
098dbe6ff4
* first commit Signed-off-by: kim <grufwub@gmail.com> * replace logging with our own log library Signed-off-by: kim <grufwub@gmail.com> * fix imports Signed-off-by: kim <grufwub@gmail.com> * fix log imports Signed-off-by: kim <grufwub@gmail.com> * add license text Signed-off-by: kim <grufwub@gmail.com> * fix package import cycle between config and log package Signed-off-by: kim <grufwub@gmail.com> * fix empty kv.Fields{} being passed to WithFields() Signed-off-by: kim <grufwub@gmail.com> * fix uses of log.WithFields() with whitespace issues and empty slices Signed-off-by: kim <grufwub@gmail.com> * *linter related grumbling* Signed-off-by: kim <grufwub@gmail.com> * gofmt the codebase! also fix more log.WithFields() formatting issues Signed-off-by: kim <grufwub@gmail.com> * update testrig code to match new changes Signed-off-by: kim <grufwub@gmail.com> * fix error wrapping in non fmt.Errorf function Signed-off-by: kim <grufwub@gmail.com> * add benchmarking of log.Caller() vs non-cached Signed-off-by: kim <grufwub@gmail.com> * fix syslog tests, add standard build tags to test runner to ensure consistency Signed-off-by: kim <grufwub@gmail.com> * make syslog tests more robust Signed-off-by: kim <grufwub@gmail.com> * fix caller depth arithmatic (is that how you spell it?) Signed-off-by: kim <grufwub@gmail.com> * update to use unkeyed fields in kv.Field{} instances Signed-off-by: kim <grufwub@gmail.com> * update go-kv library Signed-off-by: kim <grufwub@gmail.com> * update libraries list Signed-off-by: kim <grufwub@gmail.com> * fuck you linter get nerfed Signed-off-by: kim <grufwub@gmail.com> Co-authored-by: tobi <31960611+tsmethurst@users.noreply.github.com>
241 lines
7.4 KiB
Go
241 lines
7.4 KiB
Go
/*
|
|
GoToSocial
|
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package dereferencing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"codeberg.org/gruf/go-kv"
|
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
|
)
|
|
|
|
// DereferenceThread takes a statusable (something that has withReplies and withInReplyTo),
|
|
// and dereferences statusables in the conversation.
|
|
//
|
|
// This process involves working up and down the chain of replies, and parsing through the collections of IDs
|
|
// presented by remote instances as part of their replies collections, and will likely involve making several calls to
|
|
// multiple different hosts.
|
|
func (d *deref) DereferenceThread(ctx context.Context, username string, statusIRI *url.URL) error {
|
|
l := log.WithFields(kv.Fields{
|
|
|
|
{"username", username},
|
|
{"statusIRI", statusIRI},
|
|
}...)
|
|
l.Trace("entering DereferenceThread")
|
|
|
|
// if it's our status we already have everything stashed so we can bail early
|
|
if statusIRI.Host == config.GetHost() {
|
|
l.Trace("iri belongs to us, bailing")
|
|
return nil
|
|
}
|
|
|
|
// first make sure we have this status in our db
|
|
_, statusable, err := d.GetRemoteStatus(ctx, username, statusIRI, true, false)
|
|
if err != nil {
|
|
return fmt.Errorf("DereferenceThread: error getting initial status with id %s: %s", statusIRI.String(), err)
|
|
}
|
|
|
|
// first iterate up through ancestors, dereferencing if necessary as we go
|
|
if err := d.iterateAncestors(ctx, username, *statusIRI); err != nil {
|
|
return fmt.Errorf("error iterating ancestors of status %s: %s", statusIRI.String(), err)
|
|
}
|
|
|
|
// now iterate down through descendants, again dereferencing as we go
|
|
if err := d.iterateDescendants(ctx, username, *statusIRI, statusable); err != nil {
|
|
return fmt.Errorf("error iterating descendants of status %s: %s", statusIRI.String(), err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// iterateAncestors has the goal of reaching the oldest ancestor of a given status, and stashing all statuses along the way.
|
|
func (d *deref) iterateAncestors(ctx context.Context, username string, statusIRI url.URL) error {
|
|
l := log.WithFields(kv.Fields{
|
|
|
|
{"username", username},
|
|
{"statusIRI", statusIRI},
|
|
}...)
|
|
l.Trace("entering iterateAncestors")
|
|
|
|
// if it's our status we don't need to dereference anything so we can immediately move up the chain
|
|
if statusIRI.Host == config.GetHost() {
|
|
l.Trace("iri belongs to us, moving up to next ancestor")
|
|
|
|
// since this is our status, we know we can extract the id from the status path
|
|
_, id, err := uris.ParseStatusesPath(&statusIRI)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
status, err := d.db.GetStatusByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if status.InReplyToURI == "" {
|
|
// status doesn't reply to anything
|
|
return nil
|
|
}
|
|
|
|
nextIRI, err := url.Parse(status.URI)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return d.iterateAncestors(ctx, username, *nextIRI)
|
|
}
|
|
|
|
// If we reach here, we're looking at a remote status
|
|
_, statusable, err := d.GetRemoteStatus(ctx, username, &statusIRI, true, false)
|
|
if err != nil {
|
|
l.Debugf("couldn't get remote status %s: %s; can't iterate any more ancestors", statusIRI.String(), err)
|
|
return nil
|
|
}
|
|
|
|
inReplyTo := ap.ExtractInReplyToURI(statusable)
|
|
if inReplyTo == nil || inReplyTo.String() == "" {
|
|
// status doesn't reply to anything
|
|
return nil
|
|
}
|
|
|
|
// now move up to the next ancestor
|
|
return d.iterateAncestors(ctx, username, *inReplyTo)
|
|
}
|
|
|
|
func (d *deref) iterateDescendants(ctx context.Context, username string, statusIRI url.URL, statusable ap.Statusable) error {
|
|
l := log.WithFields(kv.Fields{
|
|
|
|
{"username", username},
|
|
{"statusIRI", statusIRI},
|
|
}...)
|
|
l.Trace("entering iterateDescendants")
|
|
|
|
// if it's our status we already have descendants stashed so we can bail early
|
|
if statusIRI.Host == config.GetHost() {
|
|
l.Trace("iri belongs to us, bailing")
|
|
return nil
|
|
}
|
|
|
|
replies := statusable.GetActivityStreamsReplies()
|
|
if replies == nil || !replies.IsActivityStreamsCollection() {
|
|
l.Trace("no replies, bailing")
|
|
return nil
|
|
}
|
|
|
|
repliesCollection := replies.GetActivityStreamsCollection()
|
|
if repliesCollection == nil {
|
|
l.Trace("replies collection is nil, bailing")
|
|
return nil
|
|
}
|
|
|
|
first := repliesCollection.GetActivityStreamsFirst()
|
|
if first == nil {
|
|
l.Trace("replies collection has no first, bailing")
|
|
return nil
|
|
}
|
|
|
|
firstPage := first.GetActivityStreamsCollectionPage()
|
|
if firstPage == nil {
|
|
l.Trace("first has no collection page, bailing")
|
|
return nil
|
|
}
|
|
|
|
firstPageNext := firstPage.GetActivityStreamsNext()
|
|
if firstPageNext == nil || !firstPageNext.IsIRI() {
|
|
l.Trace("next is not an iri, bailing")
|
|
return nil
|
|
}
|
|
|
|
var foundReplies int
|
|
currentPageIRI := firstPageNext.GetIRI()
|
|
|
|
pageLoop:
|
|
for {
|
|
l.Tracef("dereferencing page %s", currentPageIRI)
|
|
collectionPage, err := d.DereferenceCollectionPage(ctx, username, currentPageIRI)
|
|
if err != nil {
|
|
l.Debugf("couldn't get remote collection page %s: %s; breaking pageLoop", currentPageIRI, err)
|
|
break pageLoop
|
|
}
|
|
|
|
pageItems := collectionPage.GetActivityStreamsItems()
|
|
if pageItems.Len() == 0 {
|
|
// no items on this page, which means we're done
|
|
break pageLoop
|
|
}
|
|
|
|
// have a look through items and see what we can find
|
|
for iter := pageItems.Begin(); iter != pageItems.End(); iter = iter.Next() {
|
|
// We're looking for a url to feed to GetRemoteStatus.
|
|
// Each item can be either an IRI, or a Note.
|
|
// If a note, we grab the ID from it and call it, rather than parsing the note.
|
|
var itemURI *url.URL
|
|
switch {
|
|
case iter.IsIRI():
|
|
// iri, easy
|
|
itemURI = iter.GetIRI()
|
|
case iter.IsActivityStreamsNote():
|
|
// note, get the id from it to use as iri
|
|
note := iter.GetActivityStreamsNote()
|
|
noteID := note.GetJSONLDId()
|
|
if noteID != nil && noteID.IsIRI() {
|
|
itemURI = noteID.GetIRI()
|
|
}
|
|
default:
|
|
// if it's not an iri or a note, we don't know how to process it
|
|
continue
|
|
}
|
|
|
|
if itemURI.Host == config.GetHost() {
|
|
// skip if the reply is from us -- we already have it then
|
|
continue
|
|
}
|
|
|
|
// we can confidently say now that we found something
|
|
foundReplies++
|
|
|
|
// get the remote statusable and put it in the db
|
|
_, statusable, err := d.GetRemoteStatus(ctx, username, itemURI, true, false)
|
|
if err == nil {
|
|
// now iterate descendants of *that* status
|
|
if err := d.iterateDescendants(ctx, username, *itemURI, statusable); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
nextPage := collectionPage.GetActivityStreamsNext()
|
|
if nextPage != nil && nextPage.IsIRI() {
|
|
nextPageIRI := nextPage.GetIRI()
|
|
l.Tracef("moving on to next page %s", nextPageIRI)
|
|
currentPageIRI = nextPageIRI
|
|
} else {
|
|
l.Trace("no next page, bailing")
|
|
break pageLoop
|
|
}
|
|
}
|
|
|
|
l.Debugf("foundReplies %d", foundReplies)
|
|
return nil
|
|
}
|