Files
kim b0863b60e1 [feature] add a status visibility debug endpoint (#4682)
- adds a debug endpoint for debugging why a status may or may not be visible to you, with relevant swagger docs
- updates apiutil.NegotiateAccept() to return an errWithCode to reduce a tonne of repeated code

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4682
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
2026-02-10 14:36:03 +01:00

191 lines
6.4 KiB
Go

// 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"
"errors"
"net/url"
"time"
"code.superseriousbusiness.org/gopkg/log"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/cache"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/util"
)
func (p *Processor) DebugVisibilityGet(ctx context.Context, requester *gtsmodel.Account, statusURI string) (*apimodel.StatusVisibilityDebugResponse, gtserror.WithCode) {
// Don't leak to no-auth, also check empty.
if requester == nil || statusURI == "" {
const text = "target status not found"
return nil, gtserror.NewErrorNotFound(
errors.New(text),
text,
)
}
// Try parse string as URL obj.
uri, err := url.Parse(statusURI)
if err != nil {
return nil, gtserror.NewErrorBadRequest(
gtserror.Newf("invalid status uri: %w", err),
"invalid status uri",
)
}
// Ensure the provided URL has an acceptable scheme.
if uri.Scheme != "http" && uri.Scheme != "https" {
const text = "invalid URL scheme, acceptable schemes are http or https"
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
// Now we know we've been provided a valid URI, try fetch status.
status, _, _, err := p.federator.Dereferencer.GetStatusByURI(ctx,
requester.Username,
uri,
nil,
)
if err != nil {
log.Errorf(ctx, "error fetching status %s: %v", uri, err)
}
if status == nil {
const text = "target status not found"
return nil, gtserror.NewErrorNotFound(
errors.New(text),
text,
)
}
// Start building status vis response.
var rsp apimodel.StatusVisibilityDebugResponse
rsp.URI = status.URI
allocFilters := func() {
if rsp.Filters == nil {
rsp.Filters = new(apimodel.StatusFiltersResult)
}
}
// Get status filtering results according to fetching account, for *all* contexts.
filters, now, err := p.statusFilter.StatusFilterResults(ctx, requester, status)
if err != nil {
err := gtserror.Newf("error getting status filter results: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Append filters applied to status under each context to result.
for _, filter := range filters.Results[cache.KeyContextHome] {
allocFilters() // lazily allocate filters when needed
rsp.Filters.Home = append(rsp.Filters.Home, toFilterResult(filter, now))
}
for _, filter := range filters.Results[cache.KeyContextPublic] {
allocFilters() // lazily allocate filters when needed
rsp.Filters.Public = append(rsp.Filters.Public, toFilterResult(filter, now))
}
for _, filter := range filters.Results[cache.KeyContextNotifs] {
allocFilters() // lazily allocate filters when needed
rsp.Filters.Notifications = append(rsp.Filters.Notifications, toFilterResult(filter, now))
}
for _, filter := range filters.Results[cache.KeyContextThread] {
allocFilters() // lazily allocate filters when needed
rsp.Filters.Thread = append(rsp.Filters.Thread, toFilterResult(filter, now))
}
for _, filter := range filters.Results[cache.KeyContextAccount] {
allocFilters() // lazily allocate filters when needed
rsp.Filters.Account = append(rsp.Filters.Account, toFilterResult(filter, now))
}
// Get mute details for the status according to fetching account.
mute, err := p.muteFilter.StatusMuteDetails(ctx, requester, status)
if err != nil {
err := gtserror.Newf("error getting status mute results: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
if mute.Mute {
// Convert mute expiry time to a mute result obj.
rsp.Mute = toMuteResult(mute.MuteExpiry, now)
}
if mute.Notifications {
// Convert notifications expiry time to a notifications result obj.
rsp.MuteNotifications = toMuteResult(mute.NotificationExpiry, now)
}
// Check whether status is generally visible to the requesting authed account.
rsp.Visibility.General, err = p.visFilter.StatusVisible(ctx, requester, status)
if err != nil {
err := gtserror.Newf("error getting status visibility: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Check whether status should be visible to authed account on their public timelines.
rsp.Visibility.Public, err = p.visFilter.StatusPublicTimelineable(ctx, requester, status)
if err != nil {
err := gtserror.Newf("error getting status public visibility: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Check whether status should be visible to authed account on their home timelines.
rsp.Visibility.Home, err = p.visFilter.StatusHomeTimelineable(ctx, requester, status)
if err != nil {
err := gtserror.Newf("error getting status home visibility: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Check whether status should be visible to authed account on any tag timelines.
rsp.Visibility.Tag, err = p.visFilter.StatusTagTimelineable(ctx, requester, status)
if err != nil {
err := gtserror.Newf("error getting status tag visibility: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return &rsp, nil
}
func toFilterResult(filter cache.StatusFilterResult, now time.Time) apimodel.StatusFilterResult {
var expiry *string
active := true
if !filter.Expiry.IsZero() {
active = !filter.Expired(now)
format := util.FormatISO8601(filter.Expiry)
expiry = &format
}
return apimodel.StatusFilterResult{
Active: active,
Result: filter.Result,
Expires: expiry,
}
}
func toMuteResult(expiresAt time.Time, now time.Time) *apimodel.StatusMuteResult {
var expiry *string
active := true
if !expiresAt.IsZero() {
active = expiresAt.After(now)
format := util.FormatISO8601(expiresAt)
expiry = &format
}
return &apimodel.StatusMuteResult{
Active: active,
Expires: expiry,
}
}