mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 11:42:24 -05:00
~~Still WIP!~~ This PR allows v0.20.0 of GtS to be forward-compatible with the interaction request / authorization flow that will fully replace the current flow in v0.21.0. Basically, this means we need to recognize LikeRequest, ReplyRequest, and AnnounceRequest, and in response to those requests, deliver either a Reject or an Accept, with the latter pointing towards a LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization, respectively. This can then be used by the remote instance to prove to third parties that the interaction has been accepted by the interactee. These Authorization types need to be dereferencable to third parties, so we need to serve them. As well as recognizing the above "polite" interaction request types, we also need to still serve appropriate responses to "impolite" interaction request types, where an instance that's unaware of interaction policies tries to interact with a post by sending a reply, like, or boost directly, without wrapping it in a WhateverRequest type. Doesn't fully close https://codeberg.org/superseriousbusiness/gotosocial/issues/4026 but gets damn near (just gotta update the federating with GtS documentation). Migrations tested on both Postgres and SQLite. Co-authored-by: kim <grufwub@gmail.com> Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4394 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
537 lines
18 KiB
Go
537 lines
18 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 uris
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/config"
|
|
"code.superseriousbusiness.org/gotosocial/internal/regexes"
|
|
)
|
|
|
|
const (
|
|
UsersPath = "users" // UsersPath is for serving users info
|
|
StatusesPath = "statuses" // StatusesPath is for serving statuses
|
|
InboxPath = "inbox" // InboxPath represents the activitypub inbox location
|
|
OutboxPath = "outbox" // OutboxPath represents the activitypub outbox location
|
|
FollowersPath = "followers" // FollowersPath represents the activitypub followers location
|
|
FollowingPath = "following" // FollowingPath represents the activitypub following location
|
|
LikedPath = "liked" // LikedPath represents the activitypub liked location
|
|
CollectionsPath = "collections" // CollectionsPath represents the activitypub collections location
|
|
FeaturedPath = "featured" // FeaturedPath represents the activitypub featured location
|
|
PublicKeyPath = "main-key" // PublicKeyPath is for serving an account's public key
|
|
FollowPath = "follow" // FollowPath used to generate the URI for an individual follow or follow request
|
|
UpdatePath = "updates" // UpdatePath is used to generate the URI for an account update
|
|
BlocksPath = "blocks" // BlocksPath is used to generate the URI for a block
|
|
MovesPath = "moves" // MovesPath is used to generate the URI for a move
|
|
ReportsPath = "reports" // ReportsPath is used to generate the URI for a report/flag
|
|
ConfirmEmailPath = "confirm_email" // ConfirmEmailPath is used to generate the URI for an email confirmation link
|
|
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
|
|
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
|
|
TagsPath = "tags" // TagsPath represents the activitypub tags location
|
|
AcceptsPath = "accepts" // AcceptsPath represents the activitypub Accept's location
|
|
AuthorizationsPath = "authorizations" // AuthorizationsPath represents the location of an Authorization type such as LikeAuthorization, ReplyAuthorization, etc.
|
|
RejectsPath = "rejects" // RejectsPath represents the activitypub Reject's location
|
|
)
|
|
|
|
// UserURIs contains a bunch of UserURIs
|
|
// and URLs for a user, host, account, etc.
|
|
type UserURIs struct {
|
|
|
|
// The web URL of the instance
|
|
// host, eg https://example.org
|
|
HostURL string
|
|
|
|
// The web URL of the user,
|
|
// eg., https://example.org/@example_user
|
|
UserURL string
|
|
|
|
// The web URL for statuses of this user,
|
|
// eg., https://example.org/@example_user/statuses
|
|
StatusesURL string
|
|
|
|
// The activitypub URI of this user,
|
|
// eg., https://example.org/users/example_user
|
|
UserURI string
|
|
|
|
// The activitypub URI for this user's statuses,
|
|
// eg., https://example.org/users/example_user/statuses
|
|
StatusesURI string
|
|
|
|
// The activitypub URI for this user's activitypub inbox,
|
|
// eg., https://example.org/users/example_user/inbox
|
|
InboxURI string
|
|
|
|
// The activitypub URI for this user's activitypub outbox,
|
|
// eg., https://example.org/users/example_user/outbox
|
|
OutboxURI string
|
|
|
|
// The activitypub URI for this user's followers,
|
|
// eg., https://example.org/users/example_user/followers
|
|
FollowersURI string
|
|
|
|
// The activitypub URI for this user's following,
|
|
// eg., https://example.org/users/example_user/following
|
|
FollowingURI string
|
|
|
|
// The activitypub URI for this user's liked posts.
|
|
// eg., https://example.org/users/example_user/liked
|
|
LikedURI string
|
|
|
|
// The activitypub URI for this user's featured collections,
|
|
// eg., https://example.org/users/example_user/collections/featured
|
|
FeaturedCollectionURI string
|
|
|
|
// The URI for this user's public key,
|
|
// eg., https://example.org/users/example_user/publickey
|
|
PublicKeyURI string
|
|
}
|
|
|
|
// GenerateURIForFollow returns the AP URI for a new follow -- something like:
|
|
// https://example.org/users/whatever_user/follow/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForFollow(username string, thisFollowID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
FollowPath,
|
|
thisFollowID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForLike returns the AP URI for a new like/fave -- something like:
|
|
// https://example.org/users/whatever_user/liked/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForLike(username string, thisFavedID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
LikedPath,
|
|
thisFavedID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForUpdate returns the AP URI for a new update activity -- something like:
|
|
// https://example.org/users/whatever_user#updates/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForUpdate(username string, thisUpdateID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
UpdatePath,
|
|
thisUpdateID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForBlock returns the AP URI for a new block activity -- something like:
|
|
// https://example.org/users/whatever_user/blocks/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForBlock(username string, thisBlockID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
BlocksPath,
|
|
thisBlockID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForMove returns the AP URI for a new Move activity -- something like:
|
|
// https://example.org/users/whatever_user/moves/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForMove(username string, thisMoveID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
MovesPath,
|
|
thisMoveID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForReport returns the API URI for a new Flag activity -- something like:
|
|
// https://example.org/reports/01GP3AWY4CRDVRNZKW0TEAMB5R
|
|
//
|
|
// This path specifically doesn't contain any info about the user who did the reporting,
|
|
// to protect their privacy.
|
|
func GenerateURIForReport(thisReportID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL2(proto,
|
|
host,
|
|
ReportsPath,
|
|
thisReportID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForEmailConfirm returns a link for email confirmation -- something like:
|
|
// https://example.org/confirm_email?token=490e337c-0162-454f-ac48-4b22bb92a205
|
|
func GenerateURIForEmailConfirm(token string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL1(proto, host, ConfirmEmailPath) + "?token=" + token
|
|
}
|
|
|
|
// GenerateURIForAccept returns the AP URI for a new Accept activity -- something like:
|
|
// https://example.org/users/whatever_user/accepts/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForAccept(username string, thisAcceptID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
AcceptsPath,
|
|
thisAcceptID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForAuthorization returns the AP URI for a new Authorization object,
|
|
// ie., LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization.
|
|
// Eg., https://example.org/users/whatever_user/authorizations/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForAuthorization(username string, id string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
AuthorizationsPath,
|
|
id,
|
|
)
|
|
}
|
|
|
|
// GenerateURIForReject returns the AP URI for a new Reject activity -- something like:
|
|
// https://example.org/users/whatever_user/rejects/01F7XTH1QGBAPMGF49WJZ91XGC
|
|
func GenerateURIForReject(username string, thisRejectID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL4(proto,
|
|
host,
|
|
UsersPath,
|
|
username,
|
|
RejectsPath,
|
|
thisRejectID,
|
|
)
|
|
}
|
|
|
|
// GenerateURIsForAccount throws together a bunch of URIs
|
|
// for the given username, with the given protocol and host.
|
|
func GenerateURIsForAccount(username string) UserURIs {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
|
|
// URLs for serving web requests.
|
|
hostURL := proto + "://" + host
|
|
userURL := hostURL + "/@" + username
|
|
statusesURL := userURL + "/" + StatusesPath
|
|
|
|
// the below URIs are used in ActivityPub and Webfinger
|
|
userURI := hostURL + "/" + UsersPath + "/" + username
|
|
statusesURI := userURI + "/" + StatusesPath
|
|
inboxURI := userURI + "/" + InboxPath
|
|
outboxURI := userURI + "/" + OutboxPath
|
|
followersURI := userURI + "/" + FollowersPath
|
|
followingURI := userURI + "/" + FollowingPath
|
|
likedURI := userURI + "/" + LikedPath
|
|
collectionURI := userURI + "/" + CollectionsPath + "/" + FeaturedPath
|
|
publicKeyURI := userURI + "/" + PublicKeyPath
|
|
|
|
return UserURIs{
|
|
HostURL: hostURL,
|
|
UserURL: userURL,
|
|
StatusesURL: statusesURL,
|
|
|
|
UserURI: userURI,
|
|
StatusesURI: statusesURI,
|
|
InboxURI: inboxURI,
|
|
OutboxURI: outboxURI,
|
|
FollowersURI: followersURI,
|
|
FollowingURI: followingURI,
|
|
LikedURI: likedURI,
|
|
FeaturedCollectionURI: collectionURI,
|
|
PublicKeyURI: publicKeyURI,
|
|
}
|
|
}
|
|
|
|
// URIForAttachment generates a URI for
|
|
// an attachment/emoji/header etc.
|
|
//
|
|
// Will produce something like:
|
|
//
|
|
// "https://example.org/fileserver/01FPST95B8FC3HG3AGCDKPQNQ2/attachment/original/01FPST9QK4V5XWS3F9Z4F2G1X7.gif"
|
|
func URIForAttachment(
|
|
accountID string,
|
|
mediaType string,
|
|
mediaSize string,
|
|
mediaID string,
|
|
extension string,
|
|
) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL5(proto,
|
|
host,
|
|
FileserverPath,
|
|
accountID,
|
|
mediaType,
|
|
mediaSize,
|
|
mediaID,
|
|
) + "." + extension
|
|
}
|
|
|
|
// StoragePathForAttachment generates a storage
|
|
// path for an attachment/emoji/header etc.
|
|
//
|
|
// Will produce something like:
|
|
//
|
|
// "01FPST95B8FC3HG3AGCDKPQNQ2/attachment/original/01FPST9QK4V5XWS3F9Z4F2G1X7.gif"
|
|
func StoragePathForAttachment(
|
|
accountID string,
|
|
mediaType string,
|
|
mediaSize string,
|
|
mediaID string,
|
|
extension string,
|
|
) string {
|
|
return buildPath4(
|
|
accountID,
|
|
mediaType,
|
|
mediaSize,
|
|
mediaID,
|
|
) + "." + extension
|
|
}
|
|
|
|
// URIForEmoji generates an
|
|
// ActivityPub URI for an emoji.
|
|
//
|
|
// Will produce something like:
|
|
//
|
|
// "https://example.org/emoji/01FPST9QK4V5XWS3F9Z4F2G1X7"
|
|
func URIForEmoji(emojiID string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
return buildURL2(proto,
|
|
host,
|
|
EmojiPath,
|
|
emojiID,
|
|
)
|
|
}
|
|
|
|
// URIForTag generates an activitypub uri for a tag.
|
|
func URIForTag(name string) string {
|
|
proto := config.GetProtocol()
|
|
host := config.GetHost()
|
|
name = strings.ToLower(name)
|
|
return buildURL2(proto,
|
|
host,
|
|
TagsPath,
|
|
name,
|
|
)
|
|
}
|
|
|
|
// IsUserPath returns true if the given URL path corresponds to eg /users/example_username
|
|
func IsUserPath(id *url.URL) bool {
|
|
return regexes.UserPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsUserWebPath returns true if the given URL path corresponds to eg /@example_username
|
|
func IsUserWebPath(id *url.URL) bool {
|
|
return regexes.UserWebPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsInboxPath returns true if the given URL path corresponds to eg /users/example_username/inbox
|
|
func IsInboxPath(id *url.URL) bool {
|
|
return regexes.InboxPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsOutboxPath returns true if the given URL path corresponds to eg /users/example_username/outbox
|
|
func IsOutboxPath(id *url.URL) bool {
|
|
return regexes.OutboxPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsFollowersPath returns true if the given URL path corresponds to eg /users/example_username/followers
|
|
func IsFollowersPath(id *url.URL) bool {
|
|
return regexes.FollowersPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsFollowingPath returns true if the given URL path corresponds to eg /users/example_username/following
|
|
func IsFollowingPath(id *url.URL) bool {
|
|
return regexes.FollowingPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsFollowPath returns true if the given URL path corresponds to eg /users/example_username/follow/SOME_ULID_OF_A_FOLLOW
|
|
func IsFollowPath(id *url.URL) bool {
|
|
return regexes.FollowPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked
|
|
func IsLikedPath(id *url.URL) bool {
|
|
return regexes.LikedPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsLikePath returns true if the given URL path corresponds to eg /users/example_username/liked/SOME_ULID_OF_A_STATUS
|
|
func IsLikePath(id *url.URL) bool {
|
|
return regexes.LikePath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_ULID_OF_A_STATUS
|
|
func IsStatusesPath(id *url.URL) bool {
|
|
return regexes.StatusesPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsPublicKeyPath returns true if the given URL path corresponds to eg /users/example_username/main-key
|
|
func IsPublicKeyPath(id *url.URL) bool {
|
|
return regexes.PublicKeyPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsBlockPath returns true if the given URL path corresponds to eg /users/example_username/blocks/SOME_ULID_OF_A_BLOCK
|
|
func IsBlockPath(id *url.URL) bool {
|
|
return regexes.BlockPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsReportPath returns true if the given URL path corresponds to eg /reports/SOME_ULID_OF_A_REPORT
|
|
func IsReportPath(id *url.URL) bool {
|
|
return regexes.ReportPath.MatchString(id.Path)
|
|
}
|
|
|
|
// IsAcceptsPath returns true if the given URL path corresponds to eg /users/example_username/accepts/SOME_ULID_OF_AN_ACCEPT
|
|
func IsAcceptsPath(id *url.URL) bool {
|
|
return regexes.AcceptsPath.MatchString(id.Path)
|
|
}
|
|
|
|
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
|
|
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
|
matches := regexes.StatusesPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 3 {
|
|
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
ulid = matches[2]
|
|
return
|
|
}
|
|
|
|
// ParseUserPath returns the username from a path such as /users/example_username
|
|
func ParseUserPath(id *url.URL) (username string, err error) {
|
|
matches := regexes.UserPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 2 {
|
|
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
return
|
|
}
|
|
|
|
// ParseUserPath returns the username from a path such as /@example_username
|
|
func ParseUserWebPath(id *url.URL) (username string, err error) {
|
|
matches := regexes.UserWebPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 2 {
|
|
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
return
|
|
}
|
|
|
|
// ParseInboxPath returns the username from a path such as /users/example_username/inbox
|
|
func ParseInboxPath(id *url.URL) (username string, err error) {
|
|
matches := regexes.InboxPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 2 {
|
|
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
return
|
|
}
|
|
|
|
// ParseOutboxPath returns the username from a path such as /users/example_username/outbox
|
|
func ParseOutboxPath(id *url.URL) (username string, err error) {
|
|
matches := regexes.OutboxPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 2 {
|
|
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
return
|
|
}
|
|
|
|
// ParseFollowersPath returns the username from a path such as /users/example_username/followers
|
|
func ParseFollowersPath(id *url.URL) (username string, err error) {
|
|
matches := regexes.FollowersPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 2 {
|
|
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
return
|
|
}
|
|
|
|
// ParseFollowingPath returns the username from a path such as /users/example_username/following
|
|
func ParseFollowingPath(id *url.URL) (username string, err error) {
|
|
matches := regexes.FollowingPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 2 {
|
|
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
return
|
|
}
|
|
|
|
// ParseLikedPath returns the username and ulid from a path such as /users/example_username/liked/SOME_ULID_OF_A_STATUS
|
|
func ParseLikedPath(id *url.URL) (username string, ulid string, err error) {
|
|
matches := regexes.LikePath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 3 {
|
|
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
ulid = matches[2]
|
|
return
|
|
}
|
|
|
|
// ParseBlockPath returns the username and ulid from a path such as /users/example_username/blocks/SOME_ULID_OF_A_BLOCK
|
|
func ParseBlockPath(id *url.URL) (username string, ulid string, err error) {
|
|
matches := regexes.BlockPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 3 {
|
|
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
username = matches[1]
|
|
ulid = matches[2]
|
|
return
|
|
}
|
|
|
|
// ParseReportPath returns the ulid from a path such as /reports/SOME_ULID_OF_A_REPORT
|
|
func ParseReportPath(id *url.URL) (ulid string, err error) {
|
|
matches := regexes.ReportPath.FindStringSubmatch(id.Path)
|
|
if len(matches) != 2 {
|
|
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
|
|
return
|
|
}
|
|
ulid = matches[1]
|
|
return
|
|
}
|