diff options
Diffstat (limited to 'internal/processing/status')
| -rw-r--r-- | internal/processing/status/context.go | 566 | ||||
| -rw-r--r-- | internal/processing/status/context_test.go (renamed from internal/processing/status/get_test.go) | 147 | ||||
| -rw-r--r-- | internal/processing/status/get.go | 201 | 
3 files changed, 644 insertions, 270 deletions
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go new file mode 100644 index 000000000..4271bd233 --- /dev/null +++ b/internal/processing/status/context.go @@ -0,0 +1,566 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// 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 status + +import ( +	"context" +	"slices" +	"strings" + +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" +	"github.com/superseriousbusiness/gotosocial/internal/filter/usermute" +	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// internalThreadContext is like +// *apimodel.ThreadContext, but +// for internal use only. +type internalThreadContext struct { +	targetStatus *gtsmodel.Status +	ancestors    []*gtsmodel.Status +	descendants  []*gtsmodel.Status +} + +func (p *Processor) contextGet( +	ctx context.Context, +	requester *gtsmodel.Account, +	targetStatusID string, +) (*internalThreadContext, gtserror.WithCode) { +	targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, +		requester, +		targetStatusID, +		nil, // default freshness +	) +	if errWithCode != nil { +		return nil, errWithCode +	} + +	// Don't generate thread for boosts/reblogs. +	if targetStatus.BoostOfID != "" { +		err := gtserror.New("target status is a boost wrapper / reblog") +		return nil, gtserror.NewErrorNotFound(err) +	} + +	// Fetch up to the top of the thread. +	ancestors, err := p.state.DB.GetStatusParents(ctx, targetStatus) +	if err != nil { +		return nil, gtserror.NewErrorInternalError(err) +	} + +	// Do a simple ID sort of ancestors +	// to arrange them by creation time. +	slices.SortFunc(ancestors, func(lhs, rhs *gtsmodel.Status) int { +		return strings.Compare(lhs.ID, rhs.ID) +	}) + +	// Fetch down to the bottom of the thread. +	descendants, err := p.state.DB.GetStatusChildren(ctx, targetStatus.ID) +	if err != nil { +		return nil, gtserror.NewErrorInternalError(err) +	} + +	// Topographically sort descendants, +	// to place them in sub-threads. +	TopoSort(descendants, targetStatus.AccountID) + +	return &internalThreadContext{ +		targetStatus: targetStatus, +		ancestors:    ancestors, +		descendants:  descendants, +	}, nil +} + +// Returns true if status counts as a self-reply +// *within the current context*, ie., status is a +// self-reply by contextAcctID to contextAcctID. +func isSelfReply( +	status *gtsmodel.Status, +	contextAcctID string, +) bool { +	if status.AccountID != contextAcctID { +		// Doesn't belong +		// to context acct. +		return false +	} + +	return status.InReplyToAccountID == contextAcctID +} + +// TopoSort sorts the given slice of *descendant* +// statuses topologically, by self-reply, and by ID. +// +// "contextAcctID" should be the ID of the account that owns +// the status the thread context is being constructed around. +// +// Can handle cycles but the output order will be arbitrary. +// (But if there are cycles, something went wrong upstream.) +func TopoSort( +	statuses []*gtsmodel.Status, +	contextAcctID string, +) { +	if len(statuses) == 0 { +		return +	} + +	// Simple map of status IDs to statuses. +	// +	// Eg., +	// +	//	01J2BC6DQ37A6SQPAVCZ2BYSTN: *gtsmodel.Status +	//	01J2BC8GT9THMPWMCAZYX48PXJ: *gtsmodel.Status +	//	01J2BC8M56C5ZAH76KN93D7F0W: *gtsmodel.Status +	//	01J2BC90QNW65SM2F89R5M0NGE: *gtsmodel.Status +	//	01J2BC916YVX6D6Q0SA30JV82D: *gtsmodel.Status +	//	01J2BC91J2Y75D4Z3EEDF3DYAV: *gtsmodel.Status +	//	01J2BC91VBVPBZACZMDA7NEZY9: *gtsmodel.Status +	//	01J2BCMM3CXQE70S831YPWT48T: *gtsmodel.Status +	lookup := make(map[string]*gtsmodel.Status, len(statuses)) +	for _, status := range statuses { +		lookup[status.ID] = status +	} + +	// Tree of statuses to their children. +	// +	// The nil status may have children: any who don't +	// have a parent, or whose parent isn't in the input. +	// +	// Eg., +	// +	//	*gtsmodel.Status (01J2BC916YVX6D6Q0SA30JV82D): [     <- parent2 (1 child) +	//		*gtsmodel.Status (01J2BC91J2Y75D4Z3EEDF3DYAV)    <- p2 child1 +	//	], +	//	*gtsmodel.Status (01J2BC6DQ37A6SQPAVCZ2BYSTN): [     <- parent1 (3 children) +	//		*gtsmodel.Status (01J2BC8M56C5ZAH76KN93D7F0W)    <- p1 child3  | +	//		*gtsmodel.Status (01J2BC90QNW65SM2F89R5M0NGE)    <- p1 child1  |- Not sorted +	//		*gtsmodel.Status (01J2BC8GT9THMPWMCAZYX48PXJ)    <- p1 child2  | +	//	], +	//	*gtsmodel.Status (01J2BC91VBVPBZACZMDA7NEZY9): [     <- parent3 (no children 😢) +	//	] +	//	*gtsmodel.Status (nil): [                            <- parent4 (nil status) +	//		*gtsmodel.Status (01J2BCMM3CXQE70S831YPWT48T)    <- p4 child1 (no parent 😢) +	//	] +	tree := make(map[*gtsmodel.Status][]*gtsmodel.Status, len(statuses)) +	for _, status := range statuses { +		var parent *gtsmodel.Status +		if status.InReplyToID != "" { +			// May be nil if reply is missing. +			parent = lookup[status.InReplyToID] +		} + +		tree[parent] = append(tree[parent], status) +	} + +	// Sort children of each parent by self-reply status and then ID, *in reverse*. +	// This results in the tree looking something like: +	// +	//	*gtsmodel.Status (01J2BC916YVX6D6Q0SA30JV82D): [     <- parent2 (1 child) +	//		*gtsmodel.Status (01J2BC91J2Y75D4Z3EEDF3DYAV)    <- p2 child1 +	//	], +	//	*gtsmodel.Status (01J2BC6DQ37A6SQPAVCZ2BYSTN): [     <- parent1 (3 children) +	//		*gtsmodel.Status (01J2BC90QNW65SM2F89R5M0NGE)    <- p1 child1  | +	//		*gtsmodel.Status (01J2BC8GT9THMPWMCAZYX48PXJ)    <- p1 child2  |- Sorted +	//		*gtsmodel.Status (01J2BC8M56C5ZAH76KN93D7F0W)    <- p1 child3  | +	//	], +	//	*gtsmodel.Status (01J2BC91VBVPBZACZMDA7NEZY9): [     <- parent3 (no children 😢) +	//	], +	//	*gtsmodel.Status (nil): [                            <- parent4 (nil status) +	//		*gtsmodel.Status (01J2BCMM3CXQE70S831YPWT48T)    <- p4 child1 (no parent 😢) +	//	] +	for id, children := range tree { +		slices.SortFunc(children, func(lhs, rhs *gtsmodel.Status) int { +			lhsIsSelfReply := isSelfReply(lhs, contextAcctID) +			rhsIsSelfReply := isSelfReply(rhs, contextAcctID) + +			if lhsIsSelfReply && !rhsIsSelfReply { +				// lhs is the end +				// of a sub-thread. +				return 1 +			} else if !lhsIsSelfReply && rhsIsSelfReply { +				// lhs is the start +				// of a sub-thread. +				return -1 +			} + +			// Sort by created-at descending. +			return -strings.Compare(lhs.ID, rhs.ID) +		}) +		tree[id] = children +	} + +	// Traverse the tree using preorder depth-first +	// search, topologically sorting the statuses +	// until the stack is empty. +	// +	// The stack starts with one nil status in it +	// to account for potential nil key in the tree, +	// which means the below "for" loop will always +	// iterate at least once. +	// +	// The result will look something like: +	// +	//	*gtsmodel.Status (01J2BC6DQ37A6SQPAVCZ2BYSTN)   <- parent1 (3 children) +	//	*gtsmodel.Status (01J2BC90QNW65SM2F89R5M0NGE)   <- p1 child1  | +	//	*gtsmodel.Status (01J2BC8GT9THMPWMCAZYX48PXJ)   <- p1 child2  |- Sorted +	//	*gtsmodel.Status (01J2BC8M56C5ZAH76KN93D7F0W)   <- p1 child3  | +	//	*gtsmodel.Status (01J2BC916YVX6D6Q0SA30JV82D)   <- parent2 (1 child) +	//	*gtsmodel.Status (01J2BC91J2Y75D4Z3EEDF3DYAV)   <- p2 child1 +	//	*gtsmodel.Status (01J2BC91VBVPBZACZMDA7NEZY9)   <- parent3 (no children 😢) +	//	*gtsmodel.Status (01J2BCMM3CXQE70S831YPWT48T)   <- p4 child1 (no parent 😢) + +	stack := make([]*gtsmodel.Status, 1, len(tree)) +	statusIndex := 0 +	for len(stack) > 0 { +		parent := stack[len(stack)-1] +		children := tree[parent] + +		if len(children) == 0 { +			// No (more) children so we're +			// done with this node. +			// Remove it from the tree. +			delete(tree, parent) + +			// Also remove this node from +			// the stack, then continue +			// from its parent. +			stack = stack[:len(stack)-1] + +			continue +		} + +		// Pop the last child entry +		// (the first in sorted order). +		child := children[len(children)-1] +		tree[parent] = children[:len(children)-1] + +		// Explore its children next. +		stack = append(stack, child) + +		// Overwrite the next entry of the input slice. +		statuses[statusIndex] = child +		statusIndex++ +	} + +	// There should only be orphan nodes remaining +	// (or other nodes in the event of a cycle). +	// Append them to the end in arbitrary order. +	// +	// The fact we put them in a map first just +	// ensures the slice of statuses has no duplicates. +	for orphan := range tree { +		statuses[statusIndex] = orphan +		statusIndex++ +	} +} + +// ContextGet returns the context (previous +// and following posts) from the given status ID. +func (p *Processor) ContextGet( +	ctx context.Context, +	requester *gtsmodel.Account, +	targetStatusID string, +) (*apimodel.ThreadContext, gtserror.WithCode) { +	// Retrieve filters as they affect +	// what should be shown to requester. +	filters, err := p.state.DB.GetFiltersForAccountID( +		ctx, // Populate filters. +		requester.ID, +	) +	if err != nil { +		err = gtserror.Newf( +			"couldn't retrieve filters for account %s: %w", +			requester.ID, err, +		) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	// Retrieve mutes as they affect +	// what should be shown to requester. +	mutes, err := p.state.DB.GetAccountMutes( +		// No need to populate mutes, +		// IDs are enough here. +		gtscontext.SetBarebones(ctx), +		requester.ID, +		nil, // No paging - get all. +	) +	if err != nil { +		err = gtserror.Newf( +			"couldn't retrieve mutes for account %s: %w", +			requester.ID, err, +		) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	convert := func( +		ctx context.Context, +		status *gtsmodel.Status, +		requestingAccount *gtsmodel.Account, +	) (*apimodel.Status, error) { +		return p.converter.StatusToAPIStatus( +			ctx, +			status, +			requestingAccount, +			statusfilter.FilterContextThread, +			filters, +			usermute.NewCompiledUserMuteList(mutes), +		) +	} + +	// Retrieve the thread context. +	threadContext, errWithCode := p.contextGet( +		ctx, +		requester, +		targetStatusID, +	) +	if errWithCode != nil { +		return nil, errWithCode +	} + +	apiContext := &apimodel.ThreadContext{ +		Ancestors:   make([]apimodel.Status, 0, len(threadContext.ancestors)), +		Descendants: make([]apimodel.Status, 0, len(threadContext.descendants)), +	} + +	// Convert ancestors + filter +	// out ones that aren't visible. +	for _, status := range threadContext.ancestors { +		if v, err := p.filter.StatusVisible(ctx, requester, status); err == nil && v { +			status, err := convert(ctx, status, requester) +			if err == nil { +				apiContext.Ancestors = append(apiContext.Ancestors, *status) +			} +		} +	} + +	// Convert descendants + filter +	// out ones that aren't visible. +	for _, status := range threadContext.descendants { +		if v, err := p.filter.StatusVisible(ctx, requester, status); err == nil && v { +			status, err := convert(ctx, status, requester) +			if err == nil { +				apiContext.Descendants = append(apiContext.Descendants, *status) +			} +		} +	} + +	return apiContext, nil +} + +// WebContextGet is like ContextGet, but is explicitly +// for viewing statuses via the unauthenticated web UI. +// +// The returned statuses in the ThreadContext will be +// populated with ThreadMeta annotations for more easily +// positioning the status in a web view of a thread. +func (p *Processor) WebContextGet( +	ctx context.Context, +	targetStatusID string, +) (*apimodel.WebThreadContext, gtserror.WithCode) { +	// Retrieve the internal thread context. +	iCtx, errWithCode := p.contextGet( +		ctx, +		nil, // No authed requester. +		targetStatusID, +	) +	if errWithCode != nil { +		return nil, errWithCode +	} + +	// Recreate the whole thread so we can go +	// through it again add ThreadMeta annotations +	// from the perspective of the OG status. +	// nolint:gocritic +	wholeThread := append( +		// Ancestors at the beginning. +		iCtx.ancestors, +		append( +			// Target status in the middle. +			[]*gtsmodel.Status{iCtx.targetStatus}, +			// Descendants at the end. +			iCtx.descendants..., +		)..., +	) + +	// Start preparing web context. +	wCtx := &apimodel.WebThreadContext{ +		Ancestors:   make([]*apimodel.WebStatus, 0, len(iCtx.ancestors)), +		Descendants: make([]*apimodel.WebStatus, 0, len(iCtx.descendants)), +	} + +	var ( +		threadLength = len(wholeThread) + +		// Track how much each reply status +		// should be indented (if at all). +		statusIndents = make(map[string]int, threadLength) + +		// Who the current thread "belongs" to, +		// ie., who created first post in the thread. +		contextAcctID = wholeThread[0].AccountID + +		// Position of target status in wholeThread, +		// we put it on top of ancestors. +		targetStatusIdx = len(iCtx.ancestors) + +		// Position from which we should add +		// to descendants and not to ancestors. +		descendantsIdx = targetStatusIdx + 1 + +		// Whether we've reached end of "main" +		// thread and are now looking at replies. +		inReplies bool + +		// Index in wholeThread where +		// the "main" thread ends. +		firstReplyIdx int + +		// We should mark the next **VISIBLE** +		// reply as the first reply. +		markNextVisibleAsReply bool +	) + +	for idx, status := range wholeThread { +		if !inReplies { +			// Haven't reached end +			// of "main" thread yet. +			// +			// First post in wholeThread can't +			// be a self reply, so ignore it. +			// +			// That aside, first non-self-reply +			// in wholeThread means the "main" +			// thread is now over. +			if idx != 0 && !isSelfReply(status, contextAcctID) { +				// Jot some stuff down. +				firstReplyIdx = idx +				inReplies = true +				markNextVisibleAsReply = true +			} +		} + +		// Ensure status is actually +		// visible to just anyone. +		v, err := p.filter.StatusVisible(ctx, nil, status) +		if err != nil || !v { +			// Skip this one. +			if !inReplies { +				wCtx.ThreadHidden++ +			} else { +				wCtx.ThreadRepliesHidden++ +			} +			continue +		} + +		// Prepare status to add to thread context. +		apiStatus, err := p.converter.StatusToWebStatus(ctx, status) +		if err != nil { +			continue +		} + +		if markNextVisibleAsReply { +			// This is the first visible +			// "reply / comment", so the +			// little "x amount of replies" +			// header should go above this. +			apiStatus.ThreadFirstReply = true +			markNextVisibleAsReply = false +		} + +		// If this is a reply, work out the indent of +		// this status based on its parent's indent. +		if inReplies { +			parentIndent, ok := statusIndents[status.InReplyToID] +			switch { +			case !ok: +				// No parent with +				// indent, start at 0. +				apiStatus.Indent = 0 + +			case isSelfReply(status, status.AccountID): +				// Self reply, so indent at same +				// level as own replied-to status. +				apiStatus.Indent = parentIndent + +			case parentIndent == 5: +				// Already indented as far as we +				// can go to keep things readable +				// on thin screens, so just keep +				// parent's indent. +				apiStatus.Indent = parentIndent + +			default: +				// Reply to someone else who's +				// indented, but not to TO THE MAX. +				// Indent by another one. +				apiStatus.Indent = parentIndent + 1 +			} + +			// Store the indent for this status. +			statusIndents[status.ID] = apiStatus.Indent +		} + +		switch { +		case idx == targetStatusIdx: +			// This is the target status itself. +			wCtx.Status = apiStatus + +		case idx < descendantsIdx: +			// Haven't reached descendants yet, +			// so this must be an ancestor. +			wCtx.Ancestors = append( +				wCtx.Ancestors, +				apiStatus, +			) + +		default: +			// We're in descendants town now. +			wCtx.Descendants = append( +				wCtx.Descendants, +				apiStatus, +			) +		} +	} + +	// Now we've gone through the whole +	// thread, we can add some additional info. + +	// Length of the "main" thread. If there are +	// replies then it's up to where the replies +	// start, otherwise it's the whole thing. +	if inReplies { +		wCtx.ThreadLength = firstReplyIdx +	} else { +		wCtx.ThreadLength = threadLength +	} + +	// Jot down number of hidden posts so template doesn't have to do it. +	wCtx.ThreadShown = wCtx.ThreadLength - wCtx.ThreadHidden + +	// Number of replies is equal to number +	// of statuses in the thread that aren't +	// part of the "main" thread. +	wCtx.ThreadReplies = threadLength - wCtx.ThreadLength + +	// Jot down number of hidden replies so template doesn't have to do it. +	wCtx.ThreadRepliesShown = wCtx.ThreadReplies - wCtx.ThreadRepliesHidden + +	// Return the finished context. +	return wCtx, nil +} diff --git a/internal/processing/status/get_test.go b/internal/processing/status/context_test.go index 80482f1f2..aba58e776 100644 --- a/internal/processing/status/get_test.go +++ b/internal/processing/status/context_test.go @@ -18,17 +18,18 @@  package status_test  import ( +	"testing" +  	"github.com/stretchr/testify/suite" -	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/processing/status" -	"testing"  )  type topoSortTestSuite struct {  	suite.Suite  } -func statusIDs(apiStatuses []*apimodel.Status) []string { +func statusIDs(apiStatuses []*gtsmodel.Status) []string {  	ids := make([]string, 0, len(apiStatuses))  	for _, apiStatus := range apiStatuses {  		ids = append(ids, apiStatus.ID) @@ -38,18 +39,18 @@ func statusIDs(apiStatuses []*apimodel.Status) []string {  func (suite *topoSortTestSuite) TestBranched() {  	// https://commons.wikimedia.org/wiki/File:Sorted_binary_tree_ALL_RGB.svg -	f := &apimodel.Status{ID: "F"} -	b := &apimodel.Status{ID: "B", InReplyToID: &f.ID} -	a := &apimodel.Status{ID: "A", InReplyToID: &b.ID} -	d := &apimodel.Status{ID: "D", InReplyToID: &b.ID} -	c := &apimodel.Status{ID: "C", InReplyToID: &d.ID} -	e := &apimodel.Status{ID: "E", InReplyToID: &d.ID} -	g := &apimodel.Status{ID: "G", InReplyToID: &f.ID} -	i := &apimodel.Status{ID: "I", InReplyToID: &g.ID} -	h := &apimodel.Status{ID: "H", InReplyToID: &i.ID} - -	expected := statusIDs([]*apimodel.Status{f, b, a, d, c, e, g, i, h}) -	list := []*apimodel.Status{a, b, c, d, e, f, g, h, i} +	f := >smodel.Status{ID: "F"} +	b := >smodel.Status{ID: "B", InReplyToID: f.ID} +	a := >smodel.Status{ID: "A", InReplyToID: b.ID} +	d := >smodel.Status{ID: "D", InReplyToID: b.ID} +	c := >smodel.Status{ID: "C", InReplyToID: d.ID} +	e := >smodel.Status{ID: "E", InReplyToID: d.ID} +	g := >smodel.Status{ID: "G", InReplyToID: f.ID} +	i := >smodel.Status{ID: "I", InReplyToID: g.ID} +	h := >smodel.Status{ID: "H", InReplyToID: i.ID} + +	expected := statusIDs([]*gtsmodel.Status{f, b, a, d, c, e, g, i, h}) +	list := []*gtsmodel.Status{a, b, c, d, e, f, g, h, i}  	status.TopoSort(list, "")  	actual := statusIDs(list) @@ -57,64 +58,72 @@ func (suite *topoSortTestSuite) TestBranched() {  }  func (suite *topoSortTestSuite) TestBranchedWithSelfReplyChain() { -	targetAccount := &apimodel.Account{ID: "1"} -	otherAccount := &apimodel.Account{ID: "2"} +	targetAccount := >smodel.Account{ID: "1"} +	otherAccount := >smodel.Account{ID: "2"} -	f := &apimodel.Status{ +	f := >smodel.Status{  		ID:      "F",  		Account: targetAccount,  	} -	b := &apimodel.Status{ +	b := >smodel.Status{  		ID:                 "B",  		Account:            targetAccount, -		InReplyToID:        &f.ID, -		InReplyToAccountID: &f.Account.ID, +		AccountID:          targetAccount.ID, +		InReplyToID:        f.ID, +		InReplyToAccountID: f.Account.ID,  	} -	a := &apimodel.Status{ -		ID:                 "A", -		Account:            otherAccount, -		InReplyToID:        &b.ID, -		InReplyToAccountID: &b.Account.ID, -	} -	d := &apimodel.Status{ +	d := >smodel.Status{  		ID:                 "D",  		Account:            targetAccount, -		InReplyToID:        &b.ID, -		InReplyToAccountID: &b.Account.ID, +		AccountID:          targetAccount.ID, +		InReplyToID:        b.ID, +		InReplyToAccountID: b.Account.ID,  	} -	c := &apimodel.Status{ +	e := >smodel.Status{ +		ID:                 "E", +		Account:            targetAccount, +		AccountID:          targetAccount.ID, +		InReplyToID:        d.ID, +		InReplyToAccountID: d.Account.ID, +	} +	c := >smodel.Status{  		ID:                 "C",  		Account:            otherAccount, -		InReplyToID:        &d.ID, -		InReplyToAccountID: &d.Account.ID, +		AccountID:          otherAccount.ID, +		InReplyToID:        d.ID, +		InReplyToAccountID: d.Account.ID,  	} -	e := &apimodel.Status{ -		ID:                 "E", -		Account:            targetAccount, -		InReplyToID:        &d.ID, -		InReplyToAccountID: &d.Account.ID, +	a := >smodel.Status{ +		ID:                 "A", +		Account:            otherAccount, +		AccountID:          otherAccount.ID, +		InReplyToID:        b.ID, +		InReplyToAccountID: b.Account.ID,  	} -	g := &apimodel.Status{ +	g := >smodel.Status{  		ID:                 "G",  		Account:            otherAccount, -		InReplyToID:        &f.ID, -		InReplyToAccountID: &f.Account.ID, +		AccountID:          otherAccount.ID, +		InReplyToID:        f.ID, +		InReplyToAccountID: f.Account.ID,  	} -	i := &apimodel.Status{ +	i := >smodel.Status{  		ID:                 "I",  		Account:            targetAccount, -		InReplyToID:        &g.ID, -		InReplyToAccountID: &g.Account.ID, +		AccountID:          targetAccount.ID, +		InReplyToID:        g.ID, +		InReplyToAccountID: g.Account.ID,  	} -	h := &apimodel.Status{ +	h := >smodel.Status{  		ID:                 "H",  		Account:            otherAccount, -		InReplyToID:        &i.ID, -		InReplyToAccountID: &i.Account.ID, +		AccountID:          otherAccount.ID, +		InReplyToID:        i.ID, +		InReplyToAccountID: i.Account.ID,  	} -	expected := statusIDs([]*apimodel.Status{f, b, d, e, c, a, g, i, h}) -	list := []*apimodel.Status{a, b, c, d, e, f, g, h, i} +	expected := statusIDs([]*gtsmodel.Status{f, b, d, e, c, a, g, i, h}) +	list := []*gtsmodel.Status{a, b, c, d, e, f, g, h, i}  	status.TopoSort(list, targetAccount.ID)  	actual := statusIDs(list) @@ -122,13 +131,13 @@ func (suite *topoSortTestSuite) TestBranchedWithSelfReplyChain() {  }  func (suite *topoSortTestSuite) TestDisconnected() { -	f := &apimodel.Status{ID: "F"} -	b := &apimodel.Status{ID: "B", InReplyToID: &f.ID} +	f := >smodel.Status{ID: "F"} +	b := >smodel.Status{ID: "B", InReplyToID: f.ID}  	dID := "D" -	e := &apimodel.Status{ID: "E", InReplyToID: &dID} +	e := >smodel.Status{ID: "E", InReplyToID: dID} -	expected := statusIDs([]*apimodel.Status{e, f, b}) -	list := []*apimodel.Status{b, e, f} +	expected := statusIDs([]*gtsmodel.Status{e, f, b}) +	list := []*gtsmodel.Status{b, e, f}  	status.TopoSort(list, "")  	actual := statusIDs(list) @@ -137,10 +146,10 @@ func (suite *topoSortTestSuite) TestDisconnected() {  func (suite *topoSortTestSuite) TestTrivialCycle() {  	xID := "X" -	x := &apimodel.Status{ID: xID, InReplyToID: &xID} +	x := >smodel.Status{ID: xID, InReplyToID: xID} -	expected := statusIDs([]*apimodel.Status{x}) -	list := []*apimodel.Status{x} +	expected := statusIDs([]*gtsmodel.Status{x}) +	list := []*gtsmodel.Status{x}  	status.TopoSort(list, "")  	actual := statusIDs(list) @@ -149,11 +158,11 @@ func (suite *topoSortTestSuite) TestTrivialCycle() {  func (suite *topoSortTestSuite) TestCycle() {  	yID := "Y" -	x := &apimodel.Status{ID: "X", InReplyToID: &yID} -	y := &apimodel.Status{ID: yID, InReplyToID: &x.ID} +	x := >smodel.Status{ID: "X", InReplyToID: yID} +	y := >smodel.Status{ID: yID, InReplyToID: x.ID} -	expected := statusIDs([]*apimodel.Status{x, y}) -	list := []*apimodel.Status{x, y} +	expected := statusIDs([]*gtsmodel.Status{x, y}) +	list := []*gtsmodel.Status{x, y}  	status.TopoSort(list, "")  	actual := statusIDs(list) @@ -162,12 +171,12 @@ func (suite *topoSortTestSuite) TestCycle() {  func (suite *topoSortTestSuite) TestMixedCycle() {  	yID := "Y" -	x := &apimodel.Status{ID: "X", InReplyToID: &yID} -	y := &apimodel.Status{ID: yID, InReplyToID: &x.ID} -	z := &apimodel.Status{ID: "Z"} +	x := >smodel.Status{ID: "X", InReplyToID: yID} +	y := >smodel.Status{ID: yID, InReplyToID: x.ID} +	z := >smodel.Status{ID: "Z"} -	expected := statusIDs([]*apimodel.Status{x, y, z}) -	list := []*apimodel.Status{x, y, z} +	expected := statusIDs([]*gtsmodel.Status{x, y, z}) +	list := []*gtsmodel.Status{x, y, z}  	status.TopoSort(list, "")  	actual := statusIDs(list) @@ -175,8 +184,8 @@ func (suite *topoSortTestSuite) TestMixedCycle() {  }  func (suite *topoSortTestSuite) TestEmpty() { -	expected := statusIDs([]*apimodel.Status{}) -	list := []*apimodel.Status{} +	expected := statusIDs([]*gtsmodel.Status{}) +	list := []*gtsmodel.Status{}  	status.TopoSort(list, "")  	actual := statusIDs(list) @@ -185,7 +194,7 @@ func (suite *topoSortTestSuite) TestEmpty() {  func (suite *topoSortTestSuite) TestNil() {  	expected := statusIDs(nil) -	var list []*apimodel.Status +	var list []*gtsmodel.Status  	status.TopoSort(list, "")  	actual := statusIDs(list) diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go index 16f55b439..75a687db2 100644 --- a/internal/processing/status/get.go +++ b/internal/processing/status/get.go @@ -19,13 +19,8 @@ package status  import (  	"context" -	"slices" -	"strings"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" -	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" -	"github.com/superseriousbusiness/gotosocial/internal/filter/usermute" -	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/util" @@ -113,199 +108,3 @@ func (p *Processor) SourceGet(ctx context.Context, requestingAccount *gtsmodel.A  	}  	return statusSource, nil  } - -// WebGet gets the given status for web use, taking account of privacy settings. -func (p *Processor) WebGet(ctx context.Context, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { -	targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, -		nil, // requester -		targetStatusID, -		nil, // default freshness -	) -	if errWithCode != nil { -		return nil, errWithCode -	} - -	webStatus, err := p.converter.StatusToWebStatus(ctx, targetStatus, nil) -	if err != nil { -		err = gtserror.Newf("error converting status: %w", err) -		return nil, gtserror.NewErrorInternalError(err) -	} -	return webStatus, nil -} - -func (p *Processor) contextGet( -	ctx context.Context, -	requestingAccount *gtsmodel.Account, -	targetStatusID string, -	convert func(context.Context, *gtsmodel.Status, *gtsmodel.Account) (*apimodel.Status, error), -) (*apimodel.Context, gtserror.WithCode) { -	targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, -		requestingAccount, -		targetStatusID, -		nil, // default freshness -	) -	if errWithCode != nil { -		return nil, errWithCode -	} - -	parents, err := p.state.DB.GetStatusParents(ctx, targetStatus) -	if err != nil { -		return nil, gtserror.NewErrorInternalError(err) -	} - -	var ancestors []*apimodel.Status -	for _, status := range parents { -		if v, err := p.filter.StatusVisible(ctx, requestingAccount, status); err == nil && v { -			apiStatus, err := convert(ctx, status, requestingAccount) -			if err == nil { -				ancestors = append(ancestors, apiStatus) -			} -		} -	} - -	slices.SortFunc(ancestors, func(lhs, rhs *apimodel.Status) int { -		return strings.Compare(lhs.ID, rhs.ID) -	}) - -	children, err := p.state.DB.GetStatusChildren(ctx, targetStatus.ID) -	if err != nil { -		return nil, gtserror.NewErrorInternalError(err) -	} - -	var descendants []*apimodel.Status -	for _, status := range children { -		if v, err := p.filter.StatusVisible(ctx, requestingAccount, status); err == nil && v { -			apiStatus, err := convert(ctx, status, requestingAccount) -			if err == nil { -				descendants = append(descendants, apiStatus) -			} -		} -	} - -	TopoSort(descendants, targetStatus.AccountID) - -	context := &apimodel.Context{ -		Ancestors:   make([]apimodel.Status, 0, len(ancestors)), -		Descendants: make([]apimodel.Status, 0, len(descendants)), -	} -	for _, ancestor := range ancestors { -		context.Ancestors = append(context.Ancestors, *ancestor) -	} -	for _, descendant := range descendants { -		context.Descendants = append(context.Descendants, *descendant) -	} - -	return context, nil -} - -// TopoSort sorts statuses topologically, by self-reply, and by ID. -// Can handle cycles but the output order will be arbitrary. -// (But if there are cycles, something went wrong upstream.) -func TopoSort(apiStatuses []*apimodel.Status, targetAccountID string) { -	if len(apiStatuses) == 0 { -		return -	} - -	// Map of status IDs to statuses. -	lookup := make(map[string]*apimodel.Status, len(apiStatuses)) -	for _, apiStatus := range apiStatuses { -		lookup[apiStatus.ID] = apiStatus -	} - -	// Tree of statuses to their children. -	// The nil status may have children: any who don't have a parent, or whose parent isn't in the input. -	tree := make(map[*apimodel.Status][]*apimodel.Status, len(apiStatuses)) -	for _, apiStatus := range apiStatuses { -		var parent *apimodel.Status -		if apiStatus.InReplyToID != nil { -			parent = lookup[*apiStatus.InReplyToID] -		} -		tree[parent] = append(tree[parent], apiStatus) -	} - -	// Sort children of each status by self-reply status and then ID, *in reverse*. -	isSelfReply := func(apiStatus *apimodel.Status) bool { -		return apiStatus.GetAccountID() == targetAccountID && -			apiStatus.InReplyToAccountID != nil && -			*apiStatus.InReplyToAccountID == targetAccountID -	} -	for id, children := range tree { -		slices.SortFunc(children, func(lhs, rhs *apimodel.Status) int { -			lhsIsContextSelfReply := isSelfReply(lhs) -			rhsIsContextSelfReply := isSelfReply(rhs) - -			if lhsIsContextSelfReply && !rhsIsContextSelfReply { -				return 1 -			} else if !lhsIsContextSelfReply && rhsIsContextSelfReply { -				return -1 -			} - -			return -strings.Compare(lhs.ID, rhs.ID) -		}) -		tree[id] = children -	} - -	// Traverse the tree using preorder depth-first search, topologically sorting the statuses. -	stack := make([]*apimodel.Status, 1, len(tree)) -	apiStatusIndex := 0 -	for len(stack) > 0 { -		parent := stack[len(stack)-1] -		children := tree[parent] - -		if len(children) == 0 { -			// Remove this node from the tree. -			delete(tree, parent) -			// Go back to this node's parent. -			stack = stack[:len(stack)-1] -			continue -		} - -		// Remove the last child entry (the first in sorted order). -		child := children[len(children)-1] -		tree[parent] = children[:len(children)-1] - -		// Explore its children next. -		stack = append(stack, child) - -		// Overwrite the next entry of the input slice. -		apiStatuses[apiStatusIndex] = child -		apiStatusIndex++ -	} - -	// There should only be nodes left in the tree in the event of a cycle. -	// Append them to the end in arbitrary order. -	// This ensures that the slice of statuses has no duplicates. -	for node := range tree { -		apiStatuses[apiStatusIndex] = node -		apiStatusIndex++ -	} -} - -// ContextGet returns the context (previous and following posts) from the given status ID. -func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { -	filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) -	if err != nil { -		err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err) -		return nil, gtserror.NewErrorInternalError(err) -	} - -	mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requestingAccount.ID, nil) -	if err != nil { -		err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", requestingAccount.ID, err) -		return nil, gtserror.NewErrorInternalError(err) -	} -	compiledMutes := usermute.NewCompiledUserMuteList(mutes) - -	convert := func(ctx context.Context, status *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, error) { -		return p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextThread, filters, compiledMutes) -	} -	return p.contextGet(ctx, requestingAccount, targetStatusID, convert) -} - -// WebContextGet is like ContextGet, but is explicitly -// for viewing statuses via the unauthenticated web UI. -// -// TODO: a more advanced threading model could be implemented here. -func (p *Processor) WebContextGet(ctx context.Context, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { -	return p.contextGet(ctx, nil, targetStatusID, p.converter.StatusToWebStatus) -}  | 
