diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go index cab9245d7..6997d582f 100644 --- a/internal/api/client/account/account.go +++ b/internal/api/client/account/account.go @@ -46,6 +46,8 @@ const ( UpdateCredentialsPath = BasePath + "/update_credentials" // GetStatusesPath is for showing an account's statuses GetStatusesPath = BasePathWithID + "/statuses" + // GetFollowersPath is for showing an account's followers + GetFollowersPath = BasePathWithID + "/followers" ) // Module implements the ClientAPIModule interface for account-related actions @@ -70,6 +72,7 @@ func (m *Module) Route(r router.Router) error { r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) + r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) return nil } diff --git a/internal/api/client/account/followers.go b/internal/api/client/account/followers.go new file mode 100644 index 000000000..3401df24c --- /dev/null +++ b/internal/api/client/account/followers.go @@ -0,0 +1,49 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package account + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// AccountFollowersGETHandler serves the followers of the requested account, if they're visible to the requester. +func (m *Module) AccountFollowersGETHandler(c *gin.Context) { + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + targetAcctID := c.Param(IDKey) + if targetAcctID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + return + } + + followers, errWithCode := m.processor.AccountFollowersGet(authed, targetAcctID) + if errWithCode != nil { + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, followers) +} diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index f8c2fdbe8..1e778af62 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -679,20 +679,23 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc } // if the target user doesn't exist (anymore) then the status also shouldn't be visible - targetUser := >smodel.User{} - if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil { - l.Debug("target user could not be selected") - if err == pg.ErrNoRows { - return false, db.ErrNoEntries{} + // note: we only do this for local users + if targetAccount.Domain == "" { + targetUser := >smodel.User{} + if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil { + l.Debug("target user could not be selected") + if err == pg.ErrNoRows { + return false, db.ErrNoEntries{} + } + return false, err } - return false, err - } - // if target user is disabled, not yet approved, or not confirmed then don't show the status - // (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) - if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { - l.Debug("target user is disabled, not approved, or not confirmed") - return false, nil + // if target user is disabled, not yet approved, or not confirmed then don't show the status + // (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) + if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { + l.Debug("target user is disabled, not approved, or not confirmed") + return false, nil + } } // If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. @@ -755,6 +758,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { + l.Debug("a block exists between requesting account and reply to account") return false, nil } } @@ -764,6 +768,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { + l.Debug("a block exists between requesting account and boosted account") return false, nil } } @@ -773,6 +778,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { + l.Debug("a block exists between requesting account and boosted reply to account") return false, nil } } @@ -782,6 +788,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { + l.Debug("a block exists between requesting account and a mentioned account") return false, nil } } @@ -800,6 +807,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc return false, err } if !follows { + l.Debug("requested status is followers only but requesting account is not a follower") return false, nil } return true, nil @@ -810,6 +818,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc return false, err } if !mutuals { + l.Debug("requested status is mutuals only but accounts aren't mufos") return false, nil } return true, nil @@ -820,6 +829,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc return true, nil // yep it's mentioned! } } + l.Debug("requesting account requests a status it's not mentioned in") return false, nil // it's not mentioned -_- } diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index c9c783f1f..eac518474 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -413,7 +413,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { fromFederatorChan <- gtsmodel.FromFederator{ APObjectType: gtsmodel.ActivityStreamsNote, APActivityType: gtsmodel.ActivityStreamsCreate, - Activity: status, + GTSModel: status, } } } diff --git a/internal/federation/federator.go b/internal/federation/federator.go index 4fe0369b9..a3b1386e4 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -42,7 +42,9 @@ type Federator interface { DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error) // GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username. // This can be used for making signed http requests. - GetTransportForUser(username string) (pub.Transport, error) + // + // If username is an empty string, our instance user's credentials will be used instead. + GetTransportForUser(username string) (transport.Transport, error) pub.CommonBehavior pub.FederatingProtocol } diff --git a/internal/federation/util.go b/internal/federation/util.go index d76ce853d..14ceaeb1d 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -33,6 +33,7 @@ import ( "github.com/go-fed/activity/streams/vocab" "github.com/go-fed/httpsig" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) @@ -221,7 +222,7 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u return nil, fmt.Errorf("type name %s not supported", t.GetTypeName()) } -func (f *federator) GetTransportForUser(username string) (pub.Transport, error) { +func (f *federator) GetTransportForUser(username string) (transport.Transport, error) { // We need an account to use to create a transport for dereferecing the signature. // If a username has been given, we can fetch the account with that username and use it. // Otherwise, we can take the instance account and use those credentials to make the request. diff --git a/internal/gtsmodel/messages.go b/internal/gtsmodel/messages.go index e19b5cac1..43f30634a 100644 --- a/internal/gtsmodel/messages.go +++ b/internal/gtsmodel/messages.go @@ -1,29 +1,29 @@ package gtsmodel -// ToClientAPI wraps a message that travels from the processor into the client API -type ToClientAPI struct { - APObjectType ActivityStreamsObject - APActivityType ActivityStreamsActivity - Activity interface{} -} +// // ToClientAPI wraps a message that travels from the processor into the client API +// type ToClientAPI struct { +// APObjectType ActivityStreamsObject +// APActivityType ActivityStreamsActivity +// Activity interface{} +// } // FromClientAPI wraps a message that travels from client API into the processor type FromClientAPI struct { APObjectType ActivityStreamsObject APActivityType ActivityStreamsActivity - Activity interface{} + GTSModel interface{} } -// ToFederator wraps a message that travels from the processor into the federator -type ToFederator struct { - APObjectType ActivityStreamsObject - APActivityType ActivityStreamsActivity - Activity interface{} -} +// // ToFederator wraps a message that travels from the processor into the federator +// type ToFederator struct { +// APObjectType ActivityStreamsObject +// APActivityType ActivityStreamsActivity +// GTSModel interface{} +// } // FromFederator wraps a message that travels from the federator into the processor type FromFederator struct { APObjectType ActivityStreamsObject APActivityType ActivityStreamsActivity - Activity interface{} + GTSModel interface{} } diff --git a/internal/media/media.go b/internal/media/handler.go similarity index 57% rename from internal/media/media.go rename to internal/media/handler.go index 84f4ef554..8bbff9c46 100644 --- a/internal/media/media.go +++ b/internal/media/handler.go @@ -19,8 +19,10 @@ package media import ( + "context" "errors" "fmt" + "net/url" "strings" "time" @@ -30,6 +32,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/storage" + "github.com/superseriousbusiness/gotosocial/internal/transport" ) // Size describes the *size* of a piece of media @@ -68,13 +71,21 @@ type Handler interface { // ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, - // and then returns information to the caller about the attachment. - ProcessLocalAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error) + // and then returns information to the caller about the attachment. It's the caller's responsibility to put the returned struct + // in the database. + ProcessAttachment(attachment []byte, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) // ProcessLocalEmoji takes a new emoji and a shortcode, cleans it up, puts it in storage, and creates a new // *gts.Emoji for it, then returns it to the caller. It's the caller's responsibility to put the returned struct // in the database. ProcessLocalEmoji(emojiBytes []byte, shortcode string) (*gtsmodel.Emoji, error) + + // ProcessRemoteAttachment takes a transport, a bare-bones current attachment, and an accountID that the attachment belongs to. + // It then dereferences the attachment (ie., fetches the attachment bytes from the remote server), ensuring that the bytes are + // the correct content type. It stores the attachment in whatever storage backend the Handler has been initalized with, and returns + // information to the caller about the new attachment. It's the caller's responsibility to put the returned struct + // in the database. + ProcessRemoteAttachment(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) } type mediaHandler struct { @@ -136,27 +147,24 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin return ma, nil } -// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, +// ProcessAttachment takes a new attachment and the owning account, checks it out, removes exif data from it, // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, // and then returns information to the caller about the attachment. -func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error) { +func (mh *mediaHandler) ProcessAttachment(attachment []byte, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) { contentType, err := parseContentType(attachment) if err != nil { return nil, err } mainType := strings.Split(contentType, "/")[0] switch mainType { - case MIMEVideo: - if !SupportedVideoType(contentType) { - return nil, fmt.Errorf("video type %s not supported", contentType) - } - if len(attachment) == 0 { - return nil, errors.New("video was of size 0") - } - if len(attachment) > mh.config.MediaConfig.MaxVideoSize { - 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 MIMEVideo: + // if !SupportedVideoType(contentType) { + // return nil, fmt.Errorf("video type %s not supported", contentType) + // } + // if len(attachment) == 0 { + // return nil, errors.New("video was of size 0") + // } + // return mh.processVideoAttachment(attachment, accountID, contentType, remoteURL) case MIMEImage: if !SupportedImageType(contentType) { return nil, fmt.Errorf("image type %s not supported", contentType) @@ -164,10 +172,7 @@ func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID stri if len(attachment) == 0 { return nil, errors.New("image was of size 0") } - if len(attachment) > mh.config.MediaConfig.MaxImageSize { - return nil, fmt.Errorf("image size %d bytes exceeded max image size of %d bytes", len(attachment), mh.config.MediaConfig.MaxImageSize) - } - return mh.processImageAttachment(attachment, accountID, contentType) + return mh.processImageAttachment(attachment, accountID, contentType, remoteURL) default: break } @@ -287,221 +292,26 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( return e, nil } -/* - HELPER FUNCTIONS -*/ - -func (mh *mediaHandler) processVideoAttachment(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) { - return nil, nil -} - -func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) { - var clean []byte - var err error - var original *imageAndMeta - var small *imageAndMeta - - switch contentType { - case MIMEJpeg, MIMEPng: - if clean, err = purgeExif(data); err != nil { - return nil, fmt.Errorf("error cleaning exif data: %s", err) - } - original, err = deriveImage(clean, contentType) - if err != nil { - return nil, fmt.Errorf("error parsing image: %s", err) - } - case MIMEGif: - clean = data - original, err = deriveGif(clean, contentType) - if err != nil { - return nil, fmt.Errorf("error parsing gif: %s", err) - } - default: - return nil, errors.New("media type unrecognized") +func (mh *mediaHandler) ProcessRemoteAttachment(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) { + if currentAttachment.RemoteURL == "" { + return nil, errors.New("no remote URL on media attachment to dereference") } - - small, err = deriveThumbnail(clean, contentType, 256, 256) + remoteIRI, err := url.Parse(currentAttachment.RemoteURL) if err != nil { - return nil, fmt.Errorf("error deriving thumbnail: %s", err) + return nil, fmt.Errorf("error parsing attachment url %s: %s", currentAttachment.RemoteURL, err) } - // now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it - extension := strings.Split(contentType, "/")[1] - newMediaID := uuid.NewString() - - URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) - originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension) - smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.jpeg", URLbase, accountID, newMediaID) // all thumbnails/smalls are encoded as jpeg - - // we store the original... - originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, Attachment, Original, newMediaID, extension) - if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { - return nil, fmt.Errorf("storage error: %s", err) - } - - // and a thumbnail... - smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.jpeg", mh.config.StorageConfig.BasePath, accountID, Attachment, Small, newMediaID) // all thumbnails/smalls are encoded as jpeg - if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { - return nil, fmt.Errorf("storage error: %s", err) - } - - ma := >smodel.MediaAttachment{ - ID: newMediaID, - StatusID: "", - URL: originalURL, - RemoteURL: "", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Type: gtsmodel.FileTypeImage, - FileMeta: gtsmodel.FileMeta{ - Original: gtsmodel.Original{ - Width: original.width, - Height: original.height, - Size: original.size, - Aspect: original.aspect, - }, - Small: gtsmodel.Small{ - Width: small.width, - Height: small.height, - Size: small.size, - Aspect: small.aspect, - }, - }, - AccountID: accountID, - Description: "", - ScheduledStatusID: "", - Blurhash: original.blurhash, - Processing: 2, - File: gtsmodel.File{ - Path: originalPath, - ContentType: contentType, - FileSize: len(original.image), - UpdatedAt: time.Now(), - }, - Thumbnail: gtsmodel.Thumbnail{ - Path: smallPath, - ContentType: MIMEJpeg, // all thumbnails/smalls are encoded as jpeg - FileSize: len(small.image), - UpdatedAt: time.Now(), - URL: smallURL, - RemoteURL: "", - }, - Avatar: false, - Header: false, - } - - return ma, nil - -} - -func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string) (*gtsmodel.MediaAttachment, error) { - var isHeader bool - var isAvatar bool - - switch mediaType { - case Header: - isHeader = true - case Avatar: - isAvatar = true - default: - return nil, errors.New("header or avatar not selected") - } - - var clean []byte - var err error - - var original *imageAndMeta - switch contentType { - case MIMEJpeg: - if clean, err = purgeExif(imageBytes); err != nil { - return nil, fmt.Errorf("error cleaning exif data: %s", err) - } - original, err = deriveImage(clean, contentType) - case MIMEPng: - if clean, err = purgeExif(imageBytes); err != nil { - return nil, fmt.Errorf("error cleaning exif data: %s", err) - } - original, err = deriveImage(clean, contentType) - case MIMEGif: - clean = imageBytes - original, err = deriveGif(clean, contentType) - default: - return nil, errors.New("media type unrecognized") + // for content type, we assume we don't know what to expect... + expectedContentType := "*/*" + if currentAttachment.File.ContentType != "" { + // ... and then narrow it down if we do + expectedContentType = currentAttachment.File.ContentType } + attachmentBytes, err := t.DereferenceMedia(context.Background(), remoteIRI, expectedContentType) if err != nil { - return nil, fmt.Errorf("error parsing image: %s", err) + return nil, fmt.Errorf("dereferencing remote media with url %s: %s", remoteIRI.String(), err) } - small, err := deriveThumbnail(clean, contentType, 256, 256) - if err != nil { - return nil, fmt.Errorf("error deriving thumbnail: %s", err) - } - - // now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it - extension := strings.Split(contentType, "/")[1] - newMediaID := uuid.NewString() - - URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) - originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, mediaType, newMediaID, extension) - smallURL := fmt.Sprintf("%s/%s/%s/small/%s.%s", URLbase, accountID, mediaType, newMediaID, extension) - - // we store the original... - originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, mediaType, Original, newMediaID, extension) - if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { - return nil, fmt.Errorf("storage error: %s", err) - } - - // and a thumbnail... - smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, mediaType, Small, newMediaID, extension) - if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { - return nil, fmt.Errorf("storage error: %s", err) - } - - ma := >smodel.MediaAttachment{ - ID: newMediaID, - StatusID: "", - URL: originalURL, - RemoteURL: "", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Type: gtsmodel.FileTypeImage, - FileMeta: gtsmodel.FileMeta{ - Original: gtsmodel.Original{ - Width: original.width, - Height: original.height, - Size: original.size, - Aspect: original.aspect, - }, - Small: gtsmodel.Small{ - Width: small.width, - Height: small.height, - Size: small.size, - Aspect: small.aspect, - }, - }, - AccountID: accountID, - Description: "", - ScheduledStatusID: "", - Blurhash: original.blurhash, - Processing: 2, - File: gtsmodel.File{ - Path: originalPath, - ContentType: contentType, - FileSize: len(original.image), - UpdatedAt: time.Now(), - }, - Thumbnail: gtsmodel.Thumbnail{ - Path: smallPath, - ContentType: contentType, - FileSize: len(small.image), - UpdatedAt: time.Now(), - URL: smallURL, - RemoteURL: "", - }, - Avatar: isAvatar, - Header: isHeader, - } - - return ma, nil + return mh.ProcessAttachment(attachmentBytes, accountID, currentAttachment.RemoteURL) } diff --git a/internal/media/media_test.go b/internal/media/handler_test.go similarity index 100% rename from internal/media/media_test.go rename to internal/media/handler_test.go diff --git a/internal/media/mock_MediaHandler.go b/internal/media/mock_MediaHandler.go deleted file mode 100644 index 10fffbba4..000000000 --- a/internal/media/mock_MediaHandler.go +++ /dev/null @@ -1,59 +0,0 @@ -// Code generated by mockery v2.7.4. DO NOT EDIT. - -package media - -import ( - mock "github.com/stretchr/testify/mock" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -// MockMediaHandler is an autogenerated mock type for the MediaHandler type -type MockMediaHandler struct { - mock.Mock -} - -// ProcessAttachment provides a mock function with given fields: img, accountID -func (_m *MockMediaHandler) ProcessAttachment(img []byte, accountID string) (*gtsmodel.MediaAttachment, error) { - ret := _m.Called(img, accountID) - - var r0 *gtsmodel.MediaAttachment - if rf, ok := ret.Get(0).(func([]byte, string) *gtsmodel.MediaAttachment); ok { - r0 = rf(img, accountID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gtsmodel.MediaAttachment) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func([]byte, string) error); ok { - r1 = rf(img, accountID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SetHeaderOrAvatarForAccountID provides a mock function with given fields: img, accountID, headerOrAvi -func (_m *MockMediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) { - ret := _m.Called(img, accountID, headerOrAvi) - - var r0 *gtsmodel.MediaAttachment - if rf, ok := ret.Get(0).(func([]byte, string, string) *gtsmodel.MediaAttachment); ok { - r0 = rf(img, accountID, headerOrAvi) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gtsmodel.MediaAttachment) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func([]byte, string, string) error); ok { - r1 = rf(img, accountID, headerOrAvi) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} diff --git a/internal/media/processicon.go b/internal/media/processicon.go new file mode 100644 index 000000000..962d1c6d8 --- /dev/null +++ b/internal/media/processicon.go @@ -0,0 +1,141 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package media + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string) (*gtsmodel.MediaAttachment, error) { + var isHeader bool + var isAvatar bool + + switch mediaType { + case Header: + isHeader = true + case Avatar: + isAvatar = true + default: + return nil, errors.New("header or avatar not selected") + } + + var clean []byte + var err error + + var original *imageAndMeta + switch contentType { + case MIMEJpeg: + if clean, err = purgeExif(imageBytes); err != nil { + return nil, fmt.Errorf("error cleaning exif data: %s", err) + } + original, err = deriveImage(clean, contentType) + case MIMEPng: + if clean, err = purgeExif(imageBytes); err != nil { + return nil, fmt.Errorf("error cleaning exif data: %s", err) + } + original, err = deriveImage(clean, contentType) + case MIMEGif: + clean = imageBytes + original, err = deriveGif(clean, contentType) + default: + return nil, errors.New("media type unrecognized") + } + + if err != nil { + return nil, fmt.Errorf("error parsing image: %s", err) + } + + small, err := deriveThumbnail(clean, contentType, 256, 256) + if err != nil { + return nil, fmt.Errorf("error deriving thumbnail: %s", err) + } + + // now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it + extension := strings.Split(contentType, "/")[1] + newMediaID := uuid.NewString() + + URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) + originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, mediaType, newMediaID, extension) + smallURL := fmt.Sprintf("%s/%s/%s/small/%s.%s", URLbase, accountID, mediaType, newMediaID, extension) + + // we store the original... + originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, mediaType, Original, newMediaID, extension) + if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + + // and a thumbnail... + smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, mediaType, Small, newMediaID, extension) + if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + + ma := >smodel.MediaAttachment{ + ID: newMediaID, + StatusID: "", + URL: originalURL, + RemoteURL: "", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Type: gtsmodel.FileTypeImage, + FileMeta: gtsmodel.FileMeta{ + Original: gtsmodel.Original{ + Width: original.width, + Height: original.height, + Size: original.size, + Aspect: original.aspect, + }, + Small: gtsmodel.Small{ + Width: small.width, + Height: small.height, + Size: small.size, + Aspect: small.aspect, + }, + }, + AccountID: accountID, + Description: "", + ScheduledStatusID: "", + Blurhash: original.blurhash, + Processing: 2, + File: gtsmodel.File{ + Path: originalPath, + ContentType: contentType, + FileSize: len(original.image), + UpdatedAt: time.Now(), + }, + Thumbnail: gtsmodel.Thumbnail{ + Path: smallPath, + ContentType: contentType, + FileSize: len(small.image), + UpdatedAt: time.Now(), + URL: smallURL, + RemoteURL: "", + }, + Avatar: isAvatar, + Header: isHeader, + } + + return ma, nil +} diff --git a/internal/media/processimage.go b/internal/media/processimage.go new file mode 100644 index 000000000..dd8bff02c --- /dev/null +++ b/internal/media/processimage.go @@ -0,0 +1,128 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package media + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, contentType string, remoteURL string) (*gtsmodel.MediaAttachment, error) { + var clean []byte + var err error + var original *imageAndMeta + var small *imageAndMeta + + switch contentType { + case MIMEJpeg, MIMEPng: + if clean, err = purgeExif(data); err != nil { + return nil, fmt.Errorf("error cleaning exif data: %s", err) + } + original, err = deriveImage(clean, contentType) + if err != nil { + return nil, fmt.Errorf("error parsing image: %s", err) + } + case MIMEGif: + clean = data + original, err = deriveGif(clean, contentType) + if err != nil { + return nil, fmt.Errorf("error parsing gif: %s", err) + } + default: + return nil, errors.New("media type unrecognized") + } + + small, err = deriveThumbnail(clean, contentType, 256, 256) + if err != nil { + return nil, fmt.Errorf("error deriving thumbnail: %s", err) + } + + // now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it + extension := strings.Split(contentType, "/")[1] + newMediaID := uuid.NewString() + + URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) + originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension) + smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.jpeg", URLbase, accountID, newMediaID) // all thumbnails/smalls are encoded as jpeg + + // we store the original... + originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, Attachment, Original, newMediaID, extension) + if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + + // and a thumbnail... + smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.jpeg", mh.config.StorageConfig.BasePath, accountID, Attachment, Small, newMediaID) // all thumbnails/smalls are encoded as jpeg + if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + + ma := >smodel.MediaAttachment{ + ID: newMediaID, + StatusID: "", + URL: originalURL, + RemoteURL: remoteURL, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Type: gtsmodel.FileTypeImage, + FileMeta: gtsmodel.FileMeta{ + Original: gtsmodel.Original{ + Width: original.width, + Height: original.height, + Size: original.size, + Aspect: original.aspect, + }, + Small: gtsmodel.Small{ + Width: small.width, + Height: small.height, + Size: small.size, + Aspect: small.aspect, + }, + }, + AccountID: accountID, + Description: "", + ScheduledStatusID: "", + Blurhash: original.blurhash, + Processing: 2, + File: gtsmodel.File{ + Path: originalPath, + ContentType: contentType, + FileSize: len(original.image), + UpdatedAt: time.Now(), + }, + Thumbnail: gtsmodel.Thumbnail{ + Path: smallPath, + ContentType: MIMEJpeg, // all thumbnails/smalls are encoded as jpeg + FileSize: len(small.image), + UpdatedAt: time.Now(), + URL: smallURL, + RemoteURL: "", + }, + Avatar: false, + Header: false, + } + + return ma, nil + +} diff --git a/internal/media/processvideo.go b/internal/media/processvideo.go new file mode 100644 index 000000000..645260977 --- /dev/null +++ b/internal/media/processvideo.go @@ -0,0 +1,25 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package media + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +func (mh *mediaHandler) processVideoAttachment(data []byte, accountID string, contentType string, remoteURL string) (*gtsmodel.MediaAttachment, error) { + return nil, nil +} diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go index cf9b8b6c4..3edb871a5 100644 --- a/internal/message/accountprocess.go +++ b/internal/message/accountprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( @@ -230,3 +248,48 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin return apiStatuses, nil } + +func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) { + blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID) + if err != nil { + return nil, NewErrorInternalError(err) + } + + if blocked { + return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts")) + } + + followers := []gtsmodel.Follow{} + accounts := []apimodel.Account{} + if err := p.db.GetFollowersByAccountID(targetAccountID, &followers); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return accounts, nil + } + return nil, NewErrorInternalError(err) + } + + for _, f := range followers { + blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID) + if err != nil { + return nil, NewErrorInternalError(err) + } + if blocked { + continue + } + + a := >smodel.Account{} + if err := p.db.GetByID(f.AccountID, a); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + continue + } + return nil, NewErrorInternalError(err) + } + + account, err := p.tc.AccountToMastoPublic(a) + if err != nil { + return nil, NewErrorInternalError(err) + } + accounts = append(accounts, *account) + } + return accounts, nil +} diff --git a/internal/message/adminprocess.go b/internal/message/adminprocess.go index abf7b61c7..d26196d79 100644 --- a/internal/message/adminprocess.go +++ b/internal/message/adminprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( diff --git a/internal/message/appprocess.go b/internal/message/appprocess.go index bf56f0874..2fddb7a90 100644 --- a/internal/message/appprocess.go +++ b/internal/message/appprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( diff --git a/internal/message/error.go b/internal/message/error.go index cbd55dc78..ceeef1b41 100644 --- a/internal/message/error.go +++ b/internal/message/error.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go index 63bda46a2..3c7c30e27 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( @@ -63,7 +81,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht p.FromFederator() <- gtsmodel.FromFederator{ APObjectType: gtsmodel.ActivityStreamsProfile, APActivityType: gtsmodel.ActivityStreamsCreate, - Activity: requestingAccount, + GTSModel: requestingAccount, } return requestingAccount, nil diff --git a/internal/message/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go new file mode 100644 index 000000000..1a12216e7 --- /dev/null +++ b/internal/message/fromclientapiprocess.go @@ -0,0 +1,73 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package message + +import ( + "errors" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error { + switch clientMsg.APObjectType { + case gtsmodel.ActivityStreamsNote: + status, ok := clientMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } + + if err := p.notifyStatus(status); err != nil { + return err + } + + if status.VisibilityAdvanced.Federated { + return p.federateStatus(status) + } + return nil + } + return fmt.Errorf("message type unprocessable: %+v", clientMsg) +} + +func (p *processor) federateStatus(status *gtsmodel.Status) error { + // // derive the sending account -- it might be attached to the status already + // sendingAcct := >smodel.Account{} + // if status.GTSAccount != nil { + // sendingAcct = status.GTSAccount + // } else { + // // it wasn't attached so get it from the db instead + // if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil { + // return err + // } + // } + + // outboxURI, err := url.Parse(sendingAcct.OutboxURI) + // if err != nil { + // return err + // } + + // // convert the status to AS format Note + // note, err := p.tc.StatusToAS(status) + // if err != nil { + // return err + // } + + // _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) + return nil +} diff --git a/internal/message/fromcommonprocess.go b/internal/message/fromcommonprocess.go new file mode 100644 index 000000000..14f145df9 --- /dev/null +++ b/internal/message/fromcommonprocess.go @@ -0,0 +1,25 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package message + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +func (p *processor) notifyStatus(status *gtsmodel.Status) error { + return nil +} diff --git a/internal/message/fromfederatorprocess.go b/internal/message/fromfederatorprocess.go new file mode 100644 index 000000000..2863370d9 --- /dev/null +++ b/internal/message/fromfederatorprocess.go @@ -0,0 +1,145 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package message + +import ( + "errors" + "fmt" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/transport" +) + +func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error { + l := p.log.WithFields(logrus.Fields{ + "func": "processFromFederator", + "federatorMsg": fmt.Sprintf("%+v", federatorMsg), + }) + + l.Debug("entering function PROCESS FROM FEDERATOR") + + switch federatorMsg.APObjectType { + case gtsmodel.ActivityStreamsNote: + + incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } + + l.Debug("will now derefence incoming status") + if err := p.dereferenceStatusFields(incomingStatus); err != nil { + return fmt.Errorf("error dereferencing status from federator: %s", err) + } + if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil { + return fmt.Errorf("error updating dereferenced status in the db: %s", err) + } + + if err := p.notifyStatus(incomingStatus); err != nil { + return err + } + } + + return nil +} + +// dereferenceStatusFields fetches all the information we temporarily pinned to an incoming +// federated status, back in the federating db's Create function. +// +// When a status comes in from the federation API, there are certain fields that +// haven't been dereferenced yet, because we needed to provide a snappy synchronous +// response to the caller. By the time it reaches this function though, it's being +// processed asynchronously, so we have all the time in the world to fetch the various +// bits and bobs that are attached to the status, and properly flesh it out, before we +// send the status to any timelines and notify people. +// +// Things to dereference and fetch here: +// +// 1. Media attachments. +// 2. Hashtags. +// 3. Emojis. +// 4. Mentions. +// 5. Posting account. +// 6. Replied-to-status. +// +// SIDE EFFECTS: +// This function will deference all of the above, insert them in the database as necessary, +// and attach them to the status. The status itself will not be added to the database yet, +// that's up the caller to do. +func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { + l := p.log.WithFields(logrus.Fields{ + "func": "dereferenceStatusFields", + "status": fmt.Sprintf("%+v", status), + }) + l.Debug("entering function") + + var t transport.Transport + var err error + var username string + // TODO: dereference with a user that's addressed by the status + t, err = p.federator.GetTransportForUser(username) + if err != nil { + return fmt.Errorf("error creating transport: %s", err) + } + + // 1. Media attachments. + // + // At this point we should know: + // * the media type of the file we're looking for (a.File.ContentType) + // * the blurhash (a.Blurhash) + // * the file type (a.Type) + // * the remote URL (a.RemoteURL) + // This should be enough to pass along to the media processor. + attachmentIDs := []string{} + for _, a := range status.GTSMediaAttachments { + l.Debug("dereferencing attachment: %+v", a) + + // it might have been processed elsewhere so check first if it's already in the database or not + maybeAttachment := >smodel.MediaAttachment{} + err := p.db.GetWhere("remote_url", a.RemoteURL, maybeAttachment) + if err == nil { + // we already have it in the db, dereferenced, no need to do it again + l.Debugf("attachment already exists with id %s", maybeAttachment.ID) + attachmentIDs = append(attachmentIDs, maybeAttachment.ID) + continue + } + if _, ok := err.(db.ErrNoEntries); !ok { + // we have a real error + return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err) + } + // it just doesn't exist yet so carry on + l.Debug("attachment doesn't exist yet, calling ProcessRemoteAttachment", a) + deferencedAttachment, err := p.mediaHandler.ProcessRemoteAttachment(t, a, status.AccountID) + if err != nil { + p.log.Errorf("error dereferencing status attachment: %s", err) + continue + } + l.Debugf("dereferenced attachment: %+v", deferencedAttachment) + deferencedAttachment.StatusID = deferencedAttachment.ID + if err := p.db.Put(deferencedAttachment); err != nil { + return fmt.Errorf("error inserting dereferenced attachment with remote url %s: %s", a.RemoteURL, err) + } + deferencedAttachment.Description = a.Description + attachmentIDs = append(attachmentIDs, deferencedAttachment.ID) + } + status.Attachments = attachmentIDs + + return nil +} diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go index c96b83dec..cc3838598 100644 --- a/internal/message/frprocess.go +++ b/internal/message/frprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( diff --git a/internal/message/instanceprocess.go b/internal/message/instanceprocess.go index 0b0f15501..05ea103fd 100644 --- a/internal/message/instanceprocess.go +++ b/internal/message/instanceprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( diff --git a/internal/message/mediaprocess.go b/internal/message/mediaprocess.go index 3985849ec..094da7ace 100644 --- a/internal/message/mediaprocess.go +++ b/internal/message/mediaprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( @@ -40,7 +58,7 @@ func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentReq } // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using - attachment, err := p.mediaHandler.ProcessLocalAttachment(buf.Bytes(), authed.Account.ID) + attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID, "") if err != nil { return nil, fmt.Errorf("error reading attachment: %s", err) } diff --git a/internal/message/processor.go b/internal/message/processor.go index ae55a1502..4ed3f7af9 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -20,8 +20,6 @@ package message import ( "context" - "errors" - "fmt" "net/http" "github.com/sirupsen/logrus" @@ -44,11 +42,11 @@ import ( // for clean distribution of messages without slowing down the client API and harming the user experience. type Processor interface { // ToClientAPI returns a channel for putting in messages that need to go to the gts client API. - ToClientAPI() chan gtsmodel.ToClientAPI + // ToClientAPI() chan gtsmodel.ToClientAPI // FromClientAPI returns a channel for putting messages in that come from the client api going to the processor FromClientAPI() chan gtsmodel.FromClientAPI // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub). - ToFederator() chan gtsmodel.ToFederator + // ToFederator() chan gtsmodel.ToFederator // FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor FromFederator() chan gtsmodel.FromFederator // Start starts the Processor, reading from its channels and passing messages back and forth. @@ -70,7 +68,11 @@ type Processor interface { AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) // AccountUpdate processes the update of an account with the given form AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) + // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for + // the account given in authed. AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int) ([]apimodel.Status, ErrorWithCode) + // AccountFollowersGet + AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) @@ -142,9 +144,9 @@ type Processor interface { // processor just implements the Processor interface type processor struct { // federator pub.FederatingActor - toClientAPI chan gtsmodel.ToClientAPI + // toClientAPI chan gtsmodel.ToClientAPI fromClientAPI chan gtsmodel.FromClientAPI - toFederator chan gtsmodel.ToFederator + // toFederator chan gtsmodel.ToFederator fromFederator chan gtsmodel.FromFederator federator federation.Federator stop chan interface{} @@ -160,9 +162,9 @@ type processor struct { // NewProcessor returns a new Processor that uses the given federator and logger func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage storage.Storage, db db.DB, log *logrus.Logger) Processor { return &processor{ - toClientAPI: make(chan gtsmodel.ToClientAPI, 100), + // toClientAPI: make(chan gtsmodel.ToClientAPI, 100), fromClientAPI: make(chan gtsmodel.FromClientAPI, 100), - toFederator: make(chan gtsmodel.ToFederator, 100), + // toFederator: make(chan gtsmodel.ToFederator, 100), fromFederator: make(chan gtsmodel.FromFederator, 100), federator: federator, stop: make(chan interface{}), @@ -176,17 +178,17 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f } } -func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI { - return p.toClientAPI -} +// func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI { +// return p.toClientAPI +// } func (p *processor) FromClientAPI() chan gtsmodel.FromClientAPI { return p.fromClientAPI } -func (p *processor) ToFederator() chan gtsmodel.ToFederator { - return p.toFederator -} +// func (p *processor) ToFederator() chan gtsmodel.ToFederator { +// return p.toFederator +// } func (p *processor) FromFederator() chan gtsmodel.FromFederator { return p.fromFederator @@ -198,15 +200,15 @@ func (p *processor) Start() error { DistLoop: for { select { - case clientMsg := <-p.toClientAPI: - p.log.Infof("received message TO client API: %+v", clientMsg) + // case clientMsg := <-p.toClientAPI: + // p.log.Infof("received message TO client API: %+v", clientMsg) case clientMsg := <-p.fromClientAPI: p.log.Infof("received message FROM client API: %+v", clientMsg) if err := p.processFromClientAPI(clientMsg); err != nil { p.log.Error(err) } - case federatorMsg := <-p.toFederator: - p.log.Infof("received message TO federator: %+v", federatorMsg) + // case federatorMsg := <-p.toFederator: + // p.log.Infof("received message TO federator: %+v", federatorMsg) case federatorMsg := <-p.fromFederator: p.log.Infof("received message FROM federator: %+v", federatorMsg) if err := p.processFromFederator(federatorMsg); err != nil { @@ -226,58 +228,3 @@ func (p *processor) Stop() error { close(p.stop) return nil } - -func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error { - return nil -} - -func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error { - switch clientMsg.APObjectType { - case gtsmodel.ActivityStreamsNote: - status, ok := clientMsg.Activity.(*gtsmodel.Status) - if !ok { - return errors.New("note was not parseable as *gtsmodel.Status") - } - - if err := p.notifyStatus(status); err != nil { - return err - } - - if status.VisibilityAdvanced.Federated { - return p.federateStatus(status) - } - return nil - } - return fmt.Errorf("message type unprocessable: %+v", clientMsg) -} - -func (p *processor) federateStatus(status *gtsmodel.Status) error { - // // derive the sending account -- it might be attached to the status already - // sendingAcct := >smodel.Account{} - // if status.GTSAccount != nil { - // sendingAcct = status.GTSAccount - // } else { - // // it wasn't attached so get it from the db instead - // if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil { - // return err - // } - // } - - // outboxURI, err := url.Parse(sendingAcct.OutboxURI) - // if err != nil { - // return err - // } - - // // convert the status to AS format Note - // note, err := p.tc.StatusToAS(status) - // if err != nil { - // return err - // } - - // _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) - return nil -} - -func (p *processor) notifyStatus(status *gtsmodel.Status) error { - return nil -} diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go index 233a18ad8..676635a51 100644 --- a/internal/message/processorutil.go +++ b/internal/message/processorutil.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go index f7ca16847..0842a9018 100644 --- a/internal/message/statusprocess.go +++ b/internal/message/statusprocess.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package message import ( @@ -85,7 +103,7 @@ func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatus p.fromClientAPI <- gtsmodel.FromClientAPI{ APObjectType: newStatus.ActivityStreamsType, APActivityType: gtsmodel.ActivityStreamsCreate, - Activity: newStatus, + GTSModel: newStatus, } // return the frontend representation of the new status to the submitter diff --git a/internal/router/router.go b/internal/router/router.go index eed85771f..3e0435ecd 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -153,7 +153,7 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { s := &http.Server{ Handler: engine, ReadTimeout: 60 * time.Second, - WriteTimeout: 5 * time.Second, + WriteTimeout: 30 * time.Second, IdleTimeout: 30 * time.Second, ReadHeaderTimeout: 30 * time.Second, } diff --git a/internal/transport/controller.go b/internal/transport/controller.go index 72f41b335..ad754080a 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -21,6 +21,7 @@ package transport import ( "crypto" "fmt" + "sync" "github.com/go-fed/activity/pub" "github.com/go-fed/httpsig" @@ -30,7 +31,7 @@ import ( // Controller generates transports for use in making federation requests to other servers. type Controller interface { - NewTransport(pubKeyID string, privkey crypto.PrivateKey) (pub.Transport, error) + NewTransport(pubKeyID string, privkey crypto.PrivateKey) (Transport, error) } type controller struct { @@ -51,7 +52,7 @@ func NewController(config *config.Config, clock pub.Clock, client pub.HttpClient } // NewTransport returns a new http signature transport with the given public key id (a URL), and the given private key. -func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (pub.Transport, error) { +func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (Transport, error) { prefs := []httpsig.Algorithm{httpsig.RSA_SHA256, httpsig.RSA_SHA512} digestAlgo := httpsig.DigestSha256 getHeaders := []string{"(request-target)", "host", "date"} @@ -67,5 +68,17 @@ func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (p return nil, fmt.Errorf("error creating post signer: %s", err) } - return pub.NewHttpSigTransport(c.client, c.appAgent, c.clock, getSigner, postSigner, pubKeyID, privkey), nil + sigTransport := pub.NewHttpSigTransport(c.client, c.appAgent, c.clock, getSigner, postSigner, pubKeyID, privkey) + + return &transport{ + client: c.client, + appAgent: c.appAgent, + gofedAgent: "(go-fed/activity v1.0.0)", + clock: c.clock, + pubKeyID: pubKeyID, + privkey: privkey, + sigTransport: sigTransport, + getSigner: getSigner, + getSignerMu: &sync.Mutex{}, + }, nil } diff --git a/internal/transport/transport.go b/internal/transport/transport.go new file mode 100644 index 000000000..afd408519 --- /dev/null +++ b/internal/transport/transport.go @@ -0,0 +1,77 @@ +package transport + +import ( + "context" + "crypto" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "sync" + + "github.com/go-fed/activity/pub" + "github.com/go-fed/httpsig" +) + +// Transport wraps the pub.Transport interface with some additional +// functionality for fetching remote media. +type Transport interface { + pub.Transport + DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) +} + +// transport implements the Transport interface +type transport struct { + client pub.HttpClient + appAgent string + gofedAgent string + clock pub.Clock + pubKeyID string + privkey crypto.PrivateKey + sigTransport *pub.HttpSigTransport + getSigner httpsig.Signer + getSignerMu *sync.Mutex +} + +func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error { + return t.sigTransport.BatchDeliver(c, b, recipients) +} + +func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error { + return t.sigTransport.Deliver(c, b, to) +} + +func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) { + return t.sigTransport.Dereference(c, iri) +} + +func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) { + req, err := http.NewRequest("GET", iri.String(), nil) + if err != nil { + return nil, err + } + req = req.WithContext(c) + if expectedContentType == "" { + req.Header.Add("Accept", "*/*") + } else { + req.Header.Add("Accept", expectedContentType) + } + req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") + req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent)) + req.Header.Set("Host", iri.Host) + t.getSignerMu.Lock() + err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil) + t.getSignerMu.Unlock() + if err != nil { + return nil, err + } + resp, err := t.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status) + } + return ioutil.ReadAll(resp.Body) +} diff --git a/internal/typeutils/asextractionutil.go b/internal/typeutils/asextractionutil.go index 4ee3347bd..13cb43459 100644 --- a/internal/typeutils/asextractionutil.go +++ b/internal/typeutils/asextractionutil.go @@ -29,6 +29,7 @@ import ( "time" "github.com/go-fed/activity/pub" + "github.com/go-fed/activity/streams" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -304,12 +305,24 @@ func extractAttachments(i withAttachment) ([]*gtsmodel.MediaAttachment, error) { attachmentProp := i.GetActivityStreamsAttachment() for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() { - attachmentable, ok := iter.(Attachmentable) + + t := iter.GetType() + if t == nil { + fmt.Printf("\n\n\nGetType() nil\n\n\n") + continue + } + + m, _ := streams.Serialize(t) + fmt.Printf("\n\n\n%s\n\n\n", m) + + attachmentable, ok := t.(Attachmentable) if !ok { + fmt.Printf("\n\n\nnot attachmentable\n\n\n") continue } attachment, err := extractAttachment(attachmentable) if err != nil { + fmt.Printf("\n\n\n%s\n\n\n", err) continue } attachments = append(attachments, attachment) @@ -343,10 +356,7 @@ func extractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) { attachment.Description = name } - blurhash, err := extractBlurhash(i) - if err == nil { - attachment.Blurhash = blurhash - } + attachment.Processing = gtsmodel.ProcessingStatusReceived return attachment, nil } diff --git a/internal/typeutils/asinterfaces.go b/internal/typeutils/asinterfaces.go index 970ed2ecf..d9f180757 100644 --- a/internal/typeutils/asinterfaces.go +++ b/internal/typeutils/asinterfaces.go @@ -69,8 +69,6 @@ type Attachmentable interface { withMediaType withURL withName - withBlurhash - withFocalPoint } // Hashtaggable represents the minimum activitypub interface for representing a 'hashtag' tag. diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 7f0a4c1a4..4aa6e2b19 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -281,7 +281,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e // if it's CC'ed to public, it's public or unlocked // mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message - if isPublic(to) { + if isPublic(cc) || isPublic(to) { visibility = gtsmodel.VisibilityPublic }