From 2aaec827321ec711b98e13335899cf750f270105 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sun, 31 Oct 2021 15:46:23 +0100 Subject: smtp + email confirmation (#285) * add smtp configuration * add email confirm + reset templates * add email sender to testrig * flesh out the email sender interface * go fmt * golint * update from field with more clarity * tidy up the email formatting * fix tests * add email sender to processor * tidy client api processing a bit * further tidying in fromClientAPI * pin new account to user * send msg to processor on new account creation * generate confirm email uri * remove emailer from account processor again * add processCreateAccountFromClientAPI * move emailer accountprocessor => userprocessor * add email sender to user processor * SendConfirmEmail function * add noop email sender * use noop email sender in tests * only assemble message if callback is not nil * use noop email sender if no smtp host is defined * minify email html before sending * fix wrong email address * email confirm test * fmt * serve web hndler * add email confirm handler * init test log properly on testrig * log emails that *would* have been sent * go fmt ./... * unexport confirm email handler * updatedAt * test confirm email function * don't allow tokens older than 7 days * change error message a bit * add basic smtp docs * add a few more snippets * typo * add email sender to outbox tests * don't use dutch wikipedia link * don't minify email html --- internal/processing/fromclientapi.go | 400 +++++++++++++++++++++-------------- 1 file changed, 241 insertions(+), 159 deletions(-) (limited to 'internal/processing/fromclientapi.go') diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 3054fbf57..db6f7382d 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -36,221 +36,303 @@ func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages case ap.ActivityCreate: // CREATE switch clientMsg.APObjectType { + case ap.ObjectProfile, ap.ActorPerson: + // CREATE ACCOUNT/PROFILE + return p.processCreateAccountFromClientAPI(ctx, clientMsg) case ap.ObjectNote: // CREATE NOTE - status, ok := clientMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("note was not parseable as *gtsmodel.Status") - } - - if err := p.timelineStatus(ctx, status); err != nil { - return err - } - - if err := p.notifyStatus(ctx, status); err != nil { - return err - } - - if status.Federated { - return p.federateStatus(ctx, status) - } + return p.processCreateStatusFromClientAPI(ctx, clientMsg) case ap.ActivityFollow: // CREATE FOLLOW REQUEST - followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) - if !ok { - return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest") - } - - if err := p.notifyFollowRequest(ctx, followRequest); err != nil { - return err - } - - return p.federateFollow(ctx, followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount) + return p.processCreateFollowRequestFromClientAPI(ctx, clientMsg) case ap.ActivityLike: // CREATE LIKE/FAVE - fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) - if !ok { - return errors.New("fave was not parseable as *gtsmodel.StatusFave") - } - - if err := p.notifyFave(ctx, fave); err != nil { - return err - } - - return p.federateFave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) + return p.processCreateFaveFromClientAPI(ctx, clientMsg) case ap.ActivityAnnounce: // CREATE BOOST/ANNOUNCE - boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("boost was not parseable as *gtsmodel.Status") - } - - if err := p.timelineStatus(ctx, boostWrapperStatus); err != nil { - return err - } - - if err := p.notifyAnnounce(ctx, boostWrapperStatus); err != nil { - return err - } - - return p.federateAnnounce(ctx, boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount) + return p.processCreateAnnounceFromClientAPI(ctx, clientMsg) case ap.ActivityBlock: // CREATE BLOCK - block, ok := clientMsg.GTSModel.(*gtsmodel.Block) - if !ok { - return errors.New("block was not parseable as *gtsmodel.Block") - } - - // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa - if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.AccountID, block.TargetAccountID); err != nil { - return err - } - if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.TargetAccountID, block.AccountID); err != nil { - return err - } - - // TODO: same with notifications - // TODO: same with bookmarks - - return p.federateBlock(ctx, block) + return p.processCreateBlockFromClientAPI(ctx, clientMsg) } case ap.ActivityUpdate: // UPDATE switch clientMsg.APObjectType { case ap.ObjectProfile, ap.ActorPerson: // UPDATE ACCOUNT/PROFILE - account, ok := clientMsg.GTSModel.(*gtsmodel.Account) - if !ok { - return errors.New("account was not parseable as *gtsmodel.Account") - } - - return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount) + return p.processUpdateAccountFromClientAPI(ctx, clientMsg) } case ap.ActivityAccept: // ACCEPT switch clientMsg.APObjectType { case ap.ActivityFollow: // ACCEPT FOLLOW - follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) - if !ok { - return errors.New("accept was not parseable as *gtsmodel.Follow") - } - - if err := p.notifyFollow(ctx, follow, clientMsg.TargetAccount); err != nil { - return err - } - - return p.federateAcceptFollowRequest(ctx, follow) + return p.processAcceptFollowFromClientAPI(ctx, clientMsg) } case ap.ActivityReject: // REJECT switch clientMsg.APObjectType { case ap.ActivityFollow: // REJECT FOLLOW (request) - followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) - if !ok { - return errors.New("reject was not parseable as *gtsmodel.FollowRequest") - } - - return p.federateRejectFollowRequest(ctx, followRequest) + return p.processRejectFollowFromClientAPI(ctx, clientMsg) } case ap.ActivityUndo: // UNDO switch clientMsg.APObjectType { case ap.ActivityFollow: // UNDO FOLLOW - follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) - if !ok { - return errors.New("undo was not parseable as *gtsmodel.Follow") - } - return p.federateUnfollow(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount) + return p.processUndoFollowFromClientAPI(ctx, clientMsg) case ap.ActivityBlock: // UNDO BLOCK - block, ok := clientMsg.GTSModel.(*gtsmodel.Block) - if !ok { - return errors.New("undo was not parseable as *gtsmodel.Block") - } - return p.federateUnblock(ctx, block) + return p.processUndoBlockFromClientAPI(ctx, clientMsg) case ap.ActivityLike: // UNDO LIKE/FAVE - fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) - if !ok { - return errors.New("undo was not parseable as *gtsmodel.StatusFave") - } - return p.federateUnfave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) + return p.processUndoFaveFromClientAPI(ctx, clientMsg) case ap.ActivityAnnounce: // UNDO ANNOUNCE/BOOST - boost, ok := clientMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("undo was not parseable as *gtsmodel.Status") - } - - if err := p.deleteStatusFromTimelines(ctx, boost); err != nil { - return err - } - - return p.federateUnannounce(ctx, boost, clientMsg.OriginAccount, clientMsg.TargetAccount) + return p.processUndoAnnounceFromClientAPI(ctx, clientMsg) } case ap.ActivityDelete: // DELETE switch clientMsg.APObjectType { case ap.ObjectNote: // DELETE STATUS/NOTE - statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("note was not parseable as *gtsmodel.Status") - } - - if statusToDelete.Account == nil { - statusToDelete.Account = clientMsg.OriginAccount - } - - // delete all attachments for this status - for _, a := range statusToDelete.AttachmentIDs { - if err := p.mediaProcessor.Delete(ctx, a); err != nil { - return err - } - } - - // delete all mentions for this status - for _, m := range statusToDelete.MentionIDs { - if err := p.db.DeleteByID(ctx, m, >smodel.Mention{}); err != nil { - return err - } - } - - // delete all notifications for this status - if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { - return err - } - - // delete this status from any and all timelines - if err := p.deleteStatusFromTimelines(ctx, statusToDelete); err != nil { - return err - } - - return p.federateStatusDelete(ctx, statusToDelete) + return p.processDeleteStatusFromClientAPI(ctx, clientMsg) case ap.ObjectProfile, ap.ActorPerson: // DELETE ACCOUNT/PROFILE - - // the origin of the delete could be either a domain block, or an action by another (or this) account - var origin string - if domainBlock, ok := clientMsg.GTSModel.(*gtsmodel.DomainBlock); ok { - // origin is a domain block - origin = domainBlock.ID - } else { - // origin is whichever account caused this message - origin = clientMsg.OriginAccount.ID - } - return p.accountProcessor.Delete(ctx, clientMsg.TargetAccount, origin) + return p.processDeleteAccountFromClientAPI(ctx, clientMsg) } } return nil } +func (p *processor) processCreateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + account, ok := clientMsg.GTSModel.(*gtsmodel.Account) + if !ok { + return errors.New("account was not parseable as *gtsmodel.Account") + } + + // return if the account isn't from this domain + if account.Domain != "" { + return nil + } + + // get the user this account belongs to + user := >smodel.User{} + if err := p.db.GetWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, user); err != nil { + return err + } + + // email a confirmation to this user + return p.userProcessor.SendConfirmEmail(ctx, user, account.Username) +} + +func (p *processor) processCreateStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + status, ok := clientMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } + + if err := p.timelineStatus(ctx, status); err != nil { + return err + } + + if err := p.notifyStatus(ctx, status); err != nil { + return err + } + + return p.federateStatus(ctx, status) +} + +func (p *processor) processCreateFollowRequestFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) + if !ok { + return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest") + } + + if err := p.notifyFollowRequest(ctx, followRequest); err != nil { + return err + } + + return p.federateFollow(ctx, followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount) +} + +func (p *processor) processCreateFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) + if !ok { + return errors.New("fave was not parseable as *gtsmodel.StatusFave") + } + + if err := p.notifyFave(ctx, fave); err != nil { + return err + } + + return p.federateFave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) +} + +func (p *processor) processCreateAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("boost was not parseable as *gtsmodel.Status") + } + + if err := p.timelineStatus(ctx, boostWrapperStatus); err != nil { + return err + } + + if err := p.notifyAnnounce(ctx, boostWrapperStatus); err != nil { + return err + } + + return p.federateAnnounce(ctx, boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount) +} + +func (p *processor) processCreateBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + block, ok := clientMsg.GTSModel.(*gtsmodel.Block) + if !ok { + return errors.New("block was not parseable as *gtsmodel.Block") + } + + // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa + if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.AccountID, block.TargetAccountID); err != nil { + return err + } + if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.TargetAccountID, block.AccountID); err != nil { + return err + } + + // TODO: same with notifications + // TODO: same with bookmarks + + return p.federateBlock(ctx, block) +} + +func (p *processor) processUpdateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + account, ok := clientMsg.GTSModel.(*gtsmodel.Account) + if !ok { + return errors.New("account was not parseable as *gtsmodel.Account") + } + + return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount) +} + +func (p *processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) + if !ok { + return errors.New("accept was not parseable as *gtsmodel.Follow") + } + + if err := p.notifyFollow(ctx, follow, clientMsg.TargetAccount); err != nil { + return err + } + + return p.federateAcceptFollowRequest(ctx, follow) +} + +func (p *processor) processRejectFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) + if !ok { + return errors.New("reject was not parseable as *gtsmodel.FollowRequest") + } + + return p.federateRejectFollowRequest(ctx, followRequest) +} + +func (p *processor) processUndoFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) + if !ok { + return errors.New("undo was not parseable as *gtsmodel.Follow") + } + return p.federateUnfollow(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount) +} + +func (p *processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + block, ok := clientMsg.GTSModel.(*gtsmodel.Block) + if !ok { + return errors.New("undo was not parseable as *gtsmodel.Block") + } + return p.federateUnblock(ctx, block) +} + +func (p *processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) + if !ok { + return errors.New("undo was not parseable as *gtsmodel.StatusFave") + } + return p.federateUnfave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) +} + +func (p *processor) processUndoAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + boost, ok := clientMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("undo was not parseable as *gtsmodel.Status") + } + + if err := p.deleteStatusFromTimelines(ctx, boost); err != nil { + return err + } + + return p.federateUnannounce(ctx, boost, clientMsg.OriginAccount, clientMsg.TargetAccount) +} + +func (p *processor) processDeleteStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } + + if statusToDelete.Account == nil { + statusToDelete.Account = clientMsg.OriginAccount + } + + // delete all attachments for this status + for _, a := range statusToDelete.AttachmentIDs { + if err := p.mediaProcessor.Delete(ctx, a); err != nil { + return err + } + } + + // delete all mentions for this status + for _, m := range statusToDelete.MentionIDs { + if err := p.db.DeleteByID(ctx, m, >smodel.Mention{}); err != nil { + return err + } + } + + // delete all notifications for this status + if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { + return err + } + + // delete this status from any and all timelines + if err := p.deleteStatusFromTimelines(ctx, statusToDelete); err != nil { + return err + } + + return p.federateStatusDelete(ctx, statusToDelete) +} + +func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + // the origin of the delete could be either a domain block, or an action by another (or this) account + var origin string + if domainBlock, ok := clientMsg.GTSModel.(*gtsmodel.DomainBlock); ok { + // origin is a domain block + origin = domainBlock.ID + } else { + // origin is whichever account caused this message + origin = clientMsg.OriginAccount.ID + } + return p.accountProcessor.Delete(ctx, clientMsg.TargetAccount, origin) +} + // TODO: move all the below functions into federation.Federator func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error { + // do nothing if the status shouldn't be federated + if !status.Federated { + return nil + } + if status.Account == nil { statusAccount, err := p.db.GetAccountByID(ctx, status.AccountID) if err != nil { -- cgit v1.2.3