hell yeah
This commit is contained in:
parent
276ce6cd98
commit
9e1f61c373
|
@ -34,10 +34,10 @@ const (
|
|||
UsernameKey = "username"
|
||||
// UsersBasePath is the base path for serving information about Users eg https://example.org/users
|
||||
UsersBasePath = "/" + util.UsersPath
|
||||
// UsersBasePathWithID is just the users base path with the Username key in it.
|
||||
// UsersBasePathWithUsername is just the users base path with the Username key in it.
|
||||
// Use this anywhere you need to know the username of the user being queried.
|
||||
// Eg https://example.org/users/:username
|
||||
UsersBasePathWithID = UsersBasePath + "/:" + UsernameKey
|
||||
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
|
||||
)
|
||||
|
||||
// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
|
||||
|
@ -65,6 +65,6 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger)
|
|||
|
||||
// Route satisfies the RESTAPIModule interface
|
||||
func (m *Module) Route(s router.Router) error {
|
||||
s.AttachHandler(http.MethodGet, UsersBasePathWithID, m.UsersGETHandler)
|
||||
s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package user_test
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/message"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
type UserStandardTestSuite struct {
|
||||
// standard suite interfaces
|
||||
suite.Suite
|
||||
config *config.Config
|
||||
db db.DB
|
||||
log *logrus.Logger
|
||||
tc typeutils.TypeConverter
|
||||
federator federation.Federator
|
||||
processor message.Processor
|
||||
storage storage.Storage
|
||||
|
||||
// standard suite models
|
||||
testTokens map[string]*oauth.Token
|
||||
testClients map[string]*oauth.Client
|
||||
testApplications map[string]*gtsmodel.Application
|
||||
testUsers map[string]*gtsmodel.User
|
||||
testAccounts map[string]*gtsmodel.Account
|
||||
testAttachments map[string]*gtsmodel.MediaAttachment
|
||||
testStatuses map[string]*gtsmodel.Status
|
||||
|
||||
// module being tested
|
||||
userModule *user.Module
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
package user_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type UserGetTestSuite struct {
|
||||
UserStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *UserGetTestSuite) SetupSuite() {
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testClients = testrig.NewTestClients()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
suite.testAttachments = testrig.NewTestAttachments()
|
||||
suite.testStatuses = testrig.NewTestStatuses()
|
||||
}
|
||||
|
||||
func (suite *UserGetTestSuite) SetupTest() {
|
||||
suite.config = testrig.NewTestConfig()
|
||||
suite.db = testrig.NewTestDB()
|
||||
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
||||
}
|
||||
|
||||
func (suite *UserGetTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
testrig.StandardStorageTeardown(suite.storage)
|
||||
}
|
||||
|
||||
func (suite *UserGetTestSuite) TestGetUser() {
|
||||
// the dereference we're gonna use
|
||||
signedRequest := testrig.NewTestDereferenceRequests(suite.testAccounts)["foss_satan_dereference_zork"]
|
||||
|
||||
requestingAccount := suite.testAccounts["remote_account_1"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
encodedPublicKey, err := x509.MarshalPKIXPublicKey(requestingAccount.PublicKey)
|
||||
assert.NoError(suite.T(), err)
|
||||
publicKeyBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: encodedPublicKey,
|
||||
})
|
||||
publicKeyString := strings.ReplaceAll(string(publicKeyBytes), "\n", "\\n")
|
||||
|
||||
// for this test we need the client to return the public key of the requester on the 'remote' instance
|
||||
responseBodyString := fmt.Sprintf(`
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
|
||||
"id": "%s",
|
||||
"type": "Person",
|
||||
"preferredUsername": "%s",
|
||||
"inbox": "%s",
|
||||
|
||||
"publicKey": {
|
||||
"id": "%s",
|
||||
"owner": "%s",
|
||||
"publicKeyPem": "%s"
|
||||
}
|
||||
}`, requestingAccount.URI, requestingAccount.Username, requestingAccount.InboxURI, requestingAccount.PublicKeyURI, requestingAccount.URI, publicKeyString)
|
||||
|
||||
// create a transport controller whose client will just return the response body string we specified above
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
r := ioutil.NopCloser(bytes.NewReader([]byte(responseBodyString)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: r,
|
||||
}, nil
|
||||
}))
|
||||
// get this transport controller embedded right in the user module we're testing
|
||||
federator := testrig.NewTestFederator(suite.db, tc)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator)
|
||||
userModule := user.New(suite.config, processor, suite.log).(*user.Module)
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:8080%s", strings.Replace(user.UsersBasePathWithUsername, ":username", targetAccount.Username, 1)), nil) // the endpoint we're hitting
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: user.UsernameKey,
|
||||
Value: targetAccount.Username,
|
||||
},
|
||||
}
|
||||
|
||||
// we need these headers for the request to be validated
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
ctx.Request.Header.Set("Digest", signedRequest.DigestHeader)
|
||||
|
||||
// trigger the function being tested
|
||||
userModule.UsersGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
// should be a Person
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
person, ok := t.(vocab.ActivityStreamsPerson)
|
||||
assert.True(suite.T(), ok)
|
||||
|
||||
// convert person to account
|
||||
// since this account is already known, we should get a pretty full model of it from the conversion
|
||||
a, err := suite.tc.ASPersonToAccount(person)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.EqualValues(suite.T(), targetAccount.Username, a.Username)
|
||||
}
|
||||
|
||||
func TestUserGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserGetTestSuite))
|
||||
}
|
|
@ -157,6 +157,9 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
||||
}
|
||||
if p == nil {
|
||||
return nil, errors.New("returned public key was empty")
|
||||
}
|
||||
|
||||
// do the actual authentication here!
|
||||
algo := httpsig.RSA_SHA256 // TODO: make this more robust
|
||||
|
|
|
@ -108,15 +108,15 @@ type FileType string
|
|||
|
||||
const (
|
||||
// FileTypeImage is for jpegs and pngs
|
||||
FileTypeImage FileType = "image"
|
||||
FileTypeImage FileType = "Image"
|
||||
// FileTypeGif is for native gifs and soundless videos that have been converted to gifs
|
||||
FileTypeGif FileType = "gif"
|
||||
FileTypeGif FileType = "Gif"
|
||||
// FileTypeAudio is for audio-only files (no video)
|
||||
FileTypeAudio FileType = "audio"
|
||||
FileTypeAudio FileType = "Audio"
|
||||
// FileTypeVideo is for files with audio + visual
|
||||
FileTypeVideo FileType = "video"
|
||||
FileTypeVideo FileType = "Video"
|
||||
// FileTypeUnknown is for unknown file types (surprise surprise!)
|
||||
FileTypeUnknown FileType = "unknown"
|
||||
FileTypeUnknown FileType = "Unknown"
|
||||
)
|
||||
|
||||
// FileMeta describes metadata about the actual contents of the file.
|
||||
|
|
|
@ -113,7 +113,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !supportedImageType(contentType) {
|
||||
if !SupportedImageType(contentType) {
|
||||
return nil, fmt.Errorf("%s is not an accepted image type", contentType)
|
||||
}
|
||||
|
||||
|
@ -146,8 +146,8 @@ func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID stri
|
|||
}
|
||||
mainType := strings.Split(contentType, "/")[0]
|
||||
switch mainType {
|
||||
case "video":
|
||||
if !supportedVideoType(contentType) {
|
||||
case MIMEVideo:
|
||||
if !SupportedVideoType(contentType) {
|
||||
return nil, fmt.Errorf("video type %s not supported", contentType)
|
||||
}
|
||||
if len(attachment) == 0 {
|
||||
|
@ -157,8 +157,8 @@ func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID stri
|
|||
return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(attachment), mh.config.MediaConfig.MaxVideoSize)
|
||||
}
|
||||
return mh.processVideoAttachment(attachment, accountID, contentType)
|
||||
case "image":
|
||||
if !supportedImageType(contentType) {
|
||||
case MIMEImage:
|
||||
if !SupportedImageType(contentType) {
|
||||
return nil, fmt.Errorf("image type %s not supported", contentType)
|
||||
}
|
||||
if len(attachment) == 0 {
|
||||
|
@ -199,13 +199,13 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (
|
|||
return nil, fmt.Errorf("emoji size %d bytes exceeded max emoji size of %d bytes", len(emojiBytes), EmojiMaxBytes)
|
||||
}
|
||||
|
||||
// clean any exif data from image/png type but leave gifs alone
|
||||
// clean any exif data from png but leave gifs alone
|
||||
switch contentType {
|
||||
case "image/png":
|
||||
case MIMEPng:
|
||||
if clean, err = purgeExif(emojiBytes); err != nil {
|
||||
return nil, fmt.Errorf("error cleaning exif data: %s", err)
|
||||
}
|
||||
case "image/gif":
|
||||
case MIMEGif:
|
||||
clean = emojiBytes
|
||||
default:
|
||||
return nil, errors.New("media type unrecognized")
|
||||
|
@ -275,7 +275,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (
|
|||
ImagePath: emojiPath,
|
||||
ImageStaticPath: emojiStaticPath,
|
||||
ImageContentType: contentType,
|
||||
ImageStaticContentType: "image/png", // static version will always be a png
|
||||
ImageStaticContentType: MIMEPng, // static version will always be a png
|
||||
ImageFileSize: len(original.image),
|
||||
ImageStaticFileSize: len(static.image),
|
||||
ImageUpdatedAt: time.Now(),
|
||||
|
@ -302,7 +302,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
|
|||
var small *imageAndMeta
|
||||
|
||||
switch contentType {
|
||||
case "image/jpeg", "image/png":
|
||||
case MIMEJpeg, MIMEPng:
|
||||
if clean, err = purgeExif(data); err != nil {
|
||||
return nil, fmt.Errorf("error cleaning exif data: %s", err)
|
||||
}
|
||||
|
@ -310,7 +310,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing image: %s", err)
|
||||
}
|
||||
case "image/gif":
|
||||
case MIMEGif:
|
||||
clean = data
|
||||
original, err = deriveGif(clean, contentType)
|
||||
if err != nil {
|
||||
|
@ -380,7 +380,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
|
|||
},
|
||||
Thumbnail: gtsmodel.Thumbnail{
|
||||
Path: smallPath,
|
||||
ContentType: "image/jpeg", // all thumbnails/smalls are encoded as jpeg
|
||||
ContentType: MIMEJpeg, // all thumbnails/smalls are encoded as jpeg
|
||||
FileSize: len(small.image),
|
||||
UpdatedAt: time.Now(),
|
||||
URL: smallURL,
|
||||
|
@ -411,15 +411,15 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string
|
|||
var err error
|
||||
|
||||
switch contentType {
|
||||
case "image/jpeg":
|
||||
case MIMEJpeg:
|
||||
if clean, err = purgeExif(imageBytes); err != nil {
|
||||
return nil, fmt.Errorf("error cleaning exif data: %s", err)
|
||||
}
|
||||
case "image/png":
|
||||
case MIMEPng:
|
||||
if clean, err = purgeExif(imageBytes); err != nil {
|
||||
return nil, fmt.Errorf("error cleaning exif data: %s", err)
|
||||
}
|
||||
case "image/gif":
|
||||
case MIMEGif:
|
||||
clean = imageBytes
|
||||
default:
|
||||
return nil, errors.New("media type unrecognized")
|
||||
|
|
|
@ -33,6 +33,26 @@ import (
|
|||
"github.com/superseriousbusiness/exifremove/pkg/exifremove"
|
||||
)
|
||||
|
||||
const (
|
||||
// MIMEImage is the mime type for image
|
||||
MIMEImage = "image"
|
||||
// MIMEJpeg is the jpeg image mime type
|
||||
MIMEJpeg = "image/jpeg"
|
||||
// MIMEGif is the gif image mime type
|
||||
MIMEGif = "image/gif"
|
||||
// MIMEPng is the png image mime type
|
||||
MIMEPng = "image/png"
|
||||
|
||||
// MIMEVideo is the mime type for video
|
||||
MIMEVideo = "video"
|
||||
// MIMEmp4 is the mp4 video mime type
|
||||
MIMEMp4 = "video/mp4"
|
||||
// MIMEMpeg is the mpeg video mime type
|
||||
MIMEMpeg = "video/mpeg"
|
||||
// MIMEWebm is the webm video mime type
|
||||
MIMEWebm = "video/webm"
|
||||
)
|
||||
|
||||
// parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg").
|
||||
// Returns an error if the content type is not something we can process.
|
||||
func parseContentType(content []byte) (string, error) {
|
||||
|
@ -54,13 +74,13 @@ func parseContentType(content []byte) (string, error) {
|
|||
return kind.MIME.Value, nil
|
||||
}
|
||||
|
||||
// supportedImageType checks mime type of an image against a slice of accepted types,
|
||||
// SupportedImageType checks mime type of an image against a slice of accepted types,
|
||||
// and returns True if the mime type is accepted.
|
||||
func supportedImageType(mimeType string) bool {
|
||||
func SupportedImageType(mimeType string) bool {
|
||||
acceptedImageTypes := []string{
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/png",
|
||||
MIMEJpeg,
|
||||
MIMEGif,
|
||||
MIMEPng,
|
||||
}
|
||||
for _, accepted := range acceptedImageTypes {
|
||||
if mimeType == accepted {
|
||||
|
@ -70,13 +90,13 @@ func supportedImageType(mimeType string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// supportedVideoType checks mime type of a video against a slice of accepted types,
|
||||
// SupportedVideoType checks mime type of a video against a slice of accepted types,
|
||||
// and returns True if the mime type is accepted.
|
||||
func supportedVideoType(mimeType string) bool {
|
||||
func SupportedVideoType(mimeType string) bool {
|
||||
acceptedVideoTypes := []string{
|
||||
"video/mp4",
|
||||
"video/mpeg",
|
||||
"video/webm",
|
||||
MIMEMp4,
|
||||
MIMEMpeg,
|
||||
MIMEWebm,
|
||||
}
|
||||
for _, accepted := range acceptedVideoTypes {
|
||||
if mimeType == accepted {
|
||||
|
@ -89,8 +109,8 @@ func supportedVideoType(mimeType string) bool {
|
|||
// supportedEmojiType checks that the content type is image/png -- the only type supported for emoji.
|
||||
func supportedEmojiType(mimeType string) bool {
|
||||
acceptedEmojiTypes := []string{
|
||||
"image/gif",
|
||||
"image/png",
|
||||
MIMEGif,
|
||||
MIMEPng,
|
||||
}
|
||||
for _, accepted := range acceptedEmojiTypes {
|
||||
if mimeType == accepted {
|
||||
|
@ -121,7 +141,7 @@ func deriveGif(b []byte, extension string) (*imageAndMeta, error) {
|
|||
var g *gif.GIF
|
||||
var err error
|
||||
switch extension {
|
||||
case "image/gif":
|
||||
case MIMEGif:
|
||||
g, err = gif.DecodeAll(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -161,12 +181,12 @@ func deriveImage(b []byte, contentType string) (*imageAndMeta, error) {
|
|||
var err error
|
||||
|
||||
switch contentType {
|
||||
case "image/jpeg":
|
||||
case MIMEJpeg:
|
||||
i, err = jpeg.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "image/png":
|
||||
case MIMEPng:
|
||||
i, err = png.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -210,17 +230,17 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet
|
|||
var err error
|
||||
|
||||
switch contentType {
|
||||
case "image/jpeg":
|
||||
case MIMEJpeg:
|
||||
i, err = jpeg.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "image/png":
|
||||
case MIMEPng:
|
||||
i, err = png.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "image/gif":
|
||||
case MIMEGif:
|
||||
i, err = gif.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -254,12 +274,12 @@ func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) {
|
|||
var err error
|
||||
|
||||
switch contentType {
|
||||
case "image/png":
|
||||
case MIMEPng:
|
||||
i, err = png.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "image/gif":
|
||||
case MIMEGif:
|
||||
i, err = gif.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -135,10 +135,10 @@ func (suite *MediaUtilTestSuite) TestDeriveThumbnailFromJPEG() {
|
|||
}
|
||||
|
||||
func (suite *MediaUtilTestSuite) TestSupportedImageTypes() {
|
||||
ok := supportedImageType("image/jpeg")
|
||||
ok := SupportedImageType("image/jpeg")
|
||||
assert.True(suite.T(), ok)
|
||||
|
||||
ok = supportedImageType("image/bmp")
|
||||
ok = SupportedImageType("image/bmp")
|
||||
assert.False(suite.T(), ok)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,71 +1,197 @@
|
|||
package typeutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
)
|
||||
|
||||
func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) {
|
||||
// first check if we actually already know this person
|
||||
uriProp := person.GetJSONLDId()
|
||||
if uriProp == nil || !uriProp.IsIRI() {
|
||||
return nil, errors.New("no id property found on person, or id was not an iri")
|
||||
}
|
||||
uri := uriProp.GetIRI()
|
||||
|
||||
// acct := >smodel.Account{
|
||||
// URI: "",
|
||||
// URL: "",
|
||||
// ID: "",
|
||||
// Username: "",
|
||||
// Domain: "",
|
||||
// AvatarMediaAttachmentID: "",
|
||||
// AvatarRemoteURL: "",
|
||||
// HeaderMediaAttachmentID: "",
|
||||
// HeaderRemoteURL: "",
|
||||
// DisplayName: "",
|
||||
// Fields: nil,
|
||||
// Note: "",
|
||||
// Memorial: false,
|
||||
// MovedToAccountID: "",
|
||||
// CreatedAt: time.Time{},
|
||||
// UpdatedAt: time.Time{},
|
||||
// Bot: false,
|
||||
// Reason: "",
|
||||
// Locked: false,
|
||||
// Discoverable: true,
|
||||
// Privacy: "",
|
||||
// Sensitive: false,
|
||||
// Language: "",
|
||||
// LastWebfingeredAt: time.Now(),
|
||||
// InboxURI: "",
|
||||
// OutboxURI: "",
|
||||
// FollowingURI: "",
|
||||
// FollowersURI: "",
|
||||
// FeaturedCollectionURI: "",
|
||||
// ActorType: gtsmodel.ActivityStreamsPerson,
|
||||
// AlsoKnownAs: "",
|
||||
// PrivateKey: nil,
|
||||
// PublicKey: nil,
|
||||
// PublicKeyURI: "",
|
||||
// SensitizedAt: time.Time{},
|
||||
// SilencedAt: time.Time{},
|
||||
// SuspendedAt: time.Time{},
|
||||
// HideCollections: false,
|
||||
// SuspensionOrigin: "",
|
||||
// }
|
||||
acct := >smodel.Account{}
|
||||
if err := c.db.GetWhere("uri", uri.String(), acct); err == nil {
|
||||
// we already know this account so we can skip generating it
|
||||
return acct, nil
|
||||
} else {
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
// we don't know the account and there's been a real error
|
||||
return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// // ID
|
||||
// // Generate a new uuid for our particular database.
|
||||
// // This is distinct from the AP ID of the person.
|
||||
// id := uuid.NewString()
|
||||
// acct.ID = id
|
||||
// we don't know the account so we need to generate it from the person -- at least we already have the URI!
|
||||
acct = >smodel.Account{}
|
||||
acct.URI = uri.String()
|
||||
|
||||
// // Username
|
||||
// // We need this one so bail if it's not set.
|
||||
// username := person.GetActivityStreamsPreferredUsername()
|
||||
// if username == nil || username.GetXMLSchemaString() == "" {
|
||||
// return nil, errors.New("preferredusername was empty")
|
||||
// }
|
||||
// acct.Username = username.GetXMLSchemaString()
|
||||
// Username
|
||||
// We need this one so bail if it's not set.
|
||||
username, err := extractUsername(person)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't extract username: %s", err)
|
||||
}
|
||||
acct.Username = username
|
||||
|
||||
// // Domain
|
||||
// // We need this one as well
|
||||
// acct.Domain = domain
|
||||
// Domain
|
||||
// We need this one as well
|
||||
acct.Domain = uri.Host
|
||||
|
||||
return nil, nil
|
||||
// avatar aka icon
|
||||
// if this one isn't extractable in a format we recognise we'll just skip it
|
||||
if avatarURL, err := extractIconURL(person); err == nil {
|
||||
acct.AvatarRemoteURL = avatarURL.String()
|
||||
}
|
||||
|
||||
// header aka image
|
||||
// if this one isn't extractable in a format we recognise we'll just skip it
|
||||
if headerURL, err := extractImageURL(person); err == nil {
|
||||
acct.HeaderRemoteURL = headerURL.String()
|
||||
}
|
||||
|
||||
return acct, nil
|
||||
}
|
||||
|
||||
type usernameable interface {
|
||||
GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
|
||||
}
|
||||
|
||||
func extractUsername(i usernameable) (string, error) {
|
||||
u := i.GetActivityStreamsPreferredUsername()
|
||||
if u == nil || !u.IsXMLSchemaString() {
|
||||
return "", errors.New("preferredUsername was not a string")
|
||||
}
|
||||
if u.GetXMLSchemaString() == "" {
|
||||
return "", errors.New("preferredUsername was empty")
|
||||
}
|
||||
return u.GetXMLSchemaString(), nil
|
||||
}
|
||||
|
||||
type iconable interface {
|
||||
GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
|
||||
}
|
||||
|
||||
// extractIconURL extracts a URL to a supported image file from something like:
|
||||
// "icon": {
|
||||
// "mediaType": "image/jpeg",
|
||||
// "type": "Image",
|
||||
// "url": "http://example.org/path/to/some/file.jpeg"
|
||||
// },
|
||||
func extractIconURL(i iconable) (*url.URL, error) {
|
||||
iconProp := i.GetActivityStreamsIcon()
|
||||
if iconProp == nil {
|
||||
return nil, errors.New("icon property was nil")
|
||||
}
|
||||
|
||||
// icon can potentially contain multiple entries, so we iterate through all of them
|
||||
// here in order to find the first one that meets these criteria:
|
||||
// 1. is an image
|
||||
// 2. is a supported type
|
||||
// 3. has a URL so we can grab it
|
||||
for iconIter := iconProp.Begin(); iconIter != iconProp.End(); iconIter = iconIter.Next() {
|
||||
// 1. is an image
|
||||
if !iconIter.IsActivityStreamsImage() {
|
||||
continue
|
||||
}
|
||||
imageValue := iconIter.GetActivityStreamsImage()
|
||||
if imageValue == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. is a supported type
|
||||
imageType := imageValue.GetActivityStreamsMediaType()
|
||||
if imageType == nil || !media.SupportedImageType(imageType.Get()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. has a URL so we can grab it
|
||||
imageURLProp := imageValue.GetActivityStreamsUrl()
|
||||
if imageURLProp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// URL is also an iterable!
|
||||
// so let's take the first valid one we can find
|
||||
for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() {
|
||||
if !urlIter.IsIRI() {
|
||||
continue
|
||||
}
|
||||
if urlIter.GetIRI() == nil {
|
||||
continue
|
||||
}
|
||||
// found it!!!
|
||||
return urlIter.GetIRI(), nil
|
||||
}
|
||||
}
|
||||
// if we get to this point we didn't find an icon meeting our criteria :'(
|
||||
return nil, errors.New("could not extract valid image from icon")
|
||||
}
|
||||
|
||||
type imageable interface {
|
||||
GetActivityStreamsImage() vocab.ActivityStreamsImageProperty
|
||||
}
|
||||
|
||||
// extractImageURL extracts a URL to a supported image file from something like:
|
||||
// "image": {
|
||||
// "mediaType": "image/jpeg",
|
||||
// "type": "Image",
|
||||
// "url": "http://example.org/path/to/some/file.jpeg"
|
||||
// },
|
||||
func extractImageURL(i imageable) (*url.URL, error) {
|
||||
imageProp := i.GetActivityStreamsImage()
|
||||
if imageProp == nil {
|
||||
return nil, errors.New("icon property was nil")
|
||||
}
|
||||
|
||||
// icon can potentially contain multiple entries, so we iterate through all of them
|
||||
// here in order to find the first one that meets these criteria:
|
||||
// 1. is an image
|
||||
// 2. is a supported type
|
||||
// 3. has a URL so we can grab it
|
||||
for imageIter := imageProp.Begin(); imageIter != imageProp.End(); imageIter = imageIter.Next() {
|
||||
// 1. is an image
|
||||
if !imageIter.IsActivityStreamsImage() {
|
||||
continue
|
||||
}
|
||||
imageValue := imageIter.GetActivityStreamsImage()
|
||||
if imageValue == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. is a supported type
|
||||
imageType := imageValue.GetActivityStreamsMediaType()
|
||||
if imageType == nil || !media.SupportedImageType(imageType.Get()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. has a URL so we can grab it
|
||||
imageURLProp := imageValue.GetActivityStreamsUrl()
|
||||
if imageURLProp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// URL is also an iterable!
|
||||
// so let's take the first valid one we can find
|
||||
for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() {
|
||||
if !urlIter.IsIRI() {
|
||||
continue
|
||||
}
|
||||
if urlIter.GetIRI() == nil {
|
||||
continue
|
||||
}
|
||||
// found it!!!
|
||||
return urlIter.GetIRI(), nil
|
||||
}
|
||||
}
|
||||
// if we get to this point we didn't find an image meeting our criteria :'(
|
||||
return nil, errors.New("could not extract valid image from image property")
|
||||
}
|
||||
|
|
|
@ -440,14 +440,14 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
|
|||
Discoverable: true,
|
||||
Sensitive: false,
|
||||
Language: "en",
|
||||
URI: "https://fossbros-anonymous.io/users/foss_satan",
|
||||
URL: "https://fossbros-anonymous.io/@foss_satan",
|
||||
URI: "http://fossbros-anonymous.io/users/foss_satan",
|
||||
URL: "http://fossbros-anonymous.io/@foss_satan",
|
||||
LastWebfingeredAt: time.Time{},
|
||||
InboxURI: "https://fossbros-anonymous.io/users/foss_satan/inbox",
|
||||
OutboxURI: "https://fossbros-anonymous.io/users/foss_satan/outbox",
|
||||
FollowersURI: "https://fossbros-anonymous.io/users/foss_satan/followers",
|
||||
FollowingURI: "https://fossbros-anonymous.io/users/foss_satan/following",
|
||||
FeaturedCollectionURI: "https://fossbros-anonymous.io/users/foss_satan/collections/featured",
|
||||
InboxURI: "http://fossbros-anonymous.io/users/foss_satan/inbox",
|
||||
OutboxURI: "http://fossbros-anonymous.io/users/foss_satan/outbox",
|
||||
FollowersURI: "http://fossbros-anonymous.io/users/foss_satan/followers",
|
||||
FollowingURI: "http://fossbros-anonymous.io/users/foss_satan/following",
|
||||
FeaturedCollectionURI: "http://fossbros-anonymous.io/users/foss_satan/collections/featured",
|
||||
ActorType: gtsmodel.ActivityStreamsPerson,
|
||||
AlsoKnownAs: "",
|
||||
PrivateKey: nil,
|
||||
|
@ -1047,12 +1047,20 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit
|
|||
}
|
||||
}
|
||||
|
||||
func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature {
|
||||
sig, digest, date := getSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].URI))
|
||||
return map[string]ActivityWithSignature{
|
||||
"foss_satan_dereference_zork": {
|
||||
SignatureHeader: sig,
|
||||
DigestHeader: digest,
|
||||
DateHeader: date,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getSignatureForActivity does some sneaky sneaky work with a mock http client and a test transport controller, in order to derive
|
||||
// the HTTP Signature for the given activity, public key ID, private key, and destination.
|
||||
func getSignatureForActivity(activity pub.Activity, pubKeyID string, privkey crypto.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) {
|
||||
|
||||
streams.NewActivityStreamsPerson()
|
||||
|
||||
// create a client that basically just pulls the signature out of the request and sets it
|
||||
client := &mockHTTPClient{
|
||||
do: func(req *http.Request) (*http.Response, error) {
|
||||
|
@ -1093,6 +1101,39 @@ func getSignatureForActivity(activity pub.Activity, pubKeyID string, privkey cry
|
|||
return
|
||||
}
|
||||
|
||||
// getSignatureForDereference does some sneaky sneaky work with a mock http client and a test transport controller, in order to derive
|
||||
// the HTTP Signature for the given derefence GET request using public key ID, private key, and destination.
|
||||
func getSignatureForDereference(pubKeyID string, privkey crypto.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) {
|
||||
// create a client that basically just pulls the signature out of the request and sets it
|
||||
client := &mockHTTPClient{
|
||||
do: func(req *http.Request) (*http.Response, error) {
|
||||
signatureHeader = req.Header.Get("Signature")
|
||||
digestHeader = req.Header.Get("Digest")
|
||||
dateHeader = req.Header.Get("Date")
|
||||
r := ioutil.NopCloser(bytes.NewReader([]byte{})) // we only need this so the 'close' func doesn't nil out
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: r,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// use the client to create a new transport
|
||||
c := NewTestTransportController(client)
|
||||
tp, err := c.NewTransport(pubKeyID, privkey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// trigger the delivery function, which will trigger the 'do' function of the recorder above
|
||||
if _, err := tp.Dereference(context.Background(), destination); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// headers should now be populated
|
||||
return
|
||||
}
|
||||
|
||||
// newNote returns a new activity streams note for the given parameters
|
||||
func newNote(
|
||||
noteID *url.URL,
|
||||
|
|
Loading…
Reference in New Issue