b0863b60e1
- 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>
191 lines
6.4 KiB
Go
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,
|
|
}
|
|
}
|