mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
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
This commit is contained in:
@ -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 {
|
||||
|
Reference in New Issue
Block a user