diff --git a/appview/email/notifier.go b/appview/email/notifier.go new file mode 100644 index 00000000..bbfbf622 --- /dev/null +++ b/appview/email/notifier.go @@ -0,0 +1,129 @@ +package email + +import ( + "context" + "fmt" + "log" + + securejoin "github.com/cyphar/filepath-securejoin" + "tangled.sh/tangled.sh/core/appview/config" + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/notify" + "tangled.sh/tangled.sh/core/idresolver" +) + +type EmailNotifier struct { + db *db.DB + idResolver *idresolver.Resolver + Config *config.Config + notify.BaseNotifier +} + +func NewEmailNotifier( + db *db.DB, + idResolver *idresolver.Resolver, + config *config.Config, +) notify.Notifier { + return &EmailNotifier{ + db, + idResolver, + config, + notify.BaseNotifier{}, + } +} + +var _ notify.Notifier = &EmailNotifier{} + +// TODO: yeah this is just bad design. should be moved under idResolver ore include repoResolver at first place +func (n *EmailNotifier) repoOwnerSlashName(ctx context.Context, repo *db.Repo) (string, error) { + repoOwnerID, err := n.idResolver.ResolveIdent(ctx, repo.Did) + if err != nil || repoOwnerID.Handle.IsInvalidHandle() { + return "", fmt.Errorf("resolve comment owner did: %w", err) + } + repoOwnerHandle := repoOwnerID.Handle + var repoOwnerSlashName string + if repoOwnerHandle != "" && !repoOwnerHandle.IsInvalidHandle() { + repoOwnerSlashName, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", repoOwnerHandle), repo.Name) + } else { + repoOwnerSlashName = repo.DidSlashRepo() + } + return repoOwnerSlashName, nil +} + +func (n *EmailNotifier) buildIssueEmail(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment) (Email, error) { + commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) + if err != nil || commentOwner.Handle.IsInvalidHandle() { + return Email{}, fmt.Errorf("resolve comment owner did: %w", err) + } + baseUrl := n.Config.Core.AppviewHost + repoOwnerSlashName, err := n.repoOwnerSlashName(ctx, repo) + if err != nil { + return Email{}, nil + } + url := fmt.Sprintf("%s/%s/issues/%d#comment-%d", baseUrl, repoOwnerSlashName, comment.Issue, comment.CommentId) + return Email{ + APIKey: n.Config.Resend.ApiKey, + From: n.Config.Resend.SentFrom, + Subject: fmt.Sprintf("[%s] %s (issue#%d)", repoOwnerSlashName, issue.Title, issue.IssueId), + Html: fmt.Sprintf(`

@%s mentioned you

View it on tangled.sh.`, commentOwner.Handle.String(), url), + }, nil +} + +func (n *EmailNotifier) buildPullEmail(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment) (Email, error) { + commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) + if err != nil || commentOwner.Handle.IsInvalidHandle() { + return Email{}, fmt.Errorf("resolve comment owner did: %w", err) + } + repoOwnerSlashName, err := n.repoOwnerSlashName(ctx, repo) + if err != nil { + return Email{}, nil + } + baseUrl := n.Config.Core.AppviewHost + url := fmt.Sprintf("%s/%s/pulls/%d#comment-%d", baseUrl, repoOwnerSlashName, comment.PullId, comment.ID) + return Email{ + APIKey: n.Config.Resend.ApiKey, + From: n.Config.Resend.SentFrom, + Subject: fmt.Sprintf("[%s] %s (pr#%d)", repoOwnerSlashName, pull.Title, pull.PullId), + Html: fmt.Sprintf(`

@%s mentioned you

View it on tangled.sh.`, commentOwner.Handle.String(), url), + }, nil +} + +func (n *EmailNotifier) gatherRecipientEmails(ctx context.Context, handles []string) []string { + recipients := []string{} + resolvedIdents := n.idResolver.ResolveIdents(ctx, handles) + for _, id := range resolvedIdents { + email, err := db.GetPrimaryEmail(n.db, id.DID.String()) + if err != nil { + log.Println("failed to get primary email:", err) + continue + } + recipients = append(recipients, email.Address) + } + return recipients +} + +func (n *EmailNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { + email, err := n.buildIssueEmail(ctx, repo, issue, comment) + if err != nil { + log.Println("failed to create issue-email:", err) + return + } + // TODO: get issue-subscribed user DIDs and merge with mentioned users + recipients := n.gatherRecipientEmails(ctx, mentions) + log.Println("sending email to:", recipients) + if err = SendEmail(email, recipients...); err != nil { + log.Println("error sending email:", err) + } +} + +func (n *EmailNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) { + email, err := n.buildPullEmail(ctx, repo, pull, comment) + if err != nil { + log.Println("failed to create pull-email:", err) + } + recipients := n.gatherRecipientEmails(ctx, mentions) + log.Println("sending email to:", recipients) + if err = SendEmail(email); err != nil { + log.Println("error sending email:", err) + } +} diff --git a/appview/state/state.go b/appview/state/state.go index 529abb33..b93f0e65 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -173,6 +173,7 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { // Always add the database notifier notifiers = append(notifiers, dbnotify.NewDatabaseNotifier(d, res)) + notifiers = append(notifiers, email.NewEmailNotifier(d, res, config)) // Add other notifiers in production only if !config.Core.Dev {