feat: send email notifications for missed messages

- New cron job in cluster
- Handle presence and other concerns
- Toggle in app to disable email notifications
- Handles other edge cases such as cooldown period
- Simple html email with simple message

Closes #117
This commit is contained in:
talksik
2026-04-09 14:15:34 -07:00
parent 6fe5af8d8d
commit 51c4c6c822
25 changed files with 725 additions and 29 deletions
+32
View File
@@ -35,6 +35,7 @@ type repository interface {
getMemberHumanIds(ctx context.Context, networkID string) ([]string, error)
getNetworksForHuman(ctx context.Context, humanId string) ([]*Network, error)
isMember(ctx context.Context, networkID, humanId string) (bool, error)
listAll(ctx context.Context) ([]*Network, error)
// Invitations
createInvitation(ctx context.Context, networkID, email string) error
@@ -202,6 +203,37 @@ func (r *repositoryImpl) isMember(ctx context.Context, networkID, humanId string
return isMember, err
}
func (r *repositoryImpl) listAll(ctx context.Context) ([]*Network, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, name, admin_human_id, created_at FROM networks`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var networks []*Network
for rows.Next() {
var n Network
if err := rows.Scan(&n.ID, &n.Name, &n.AdminHumanId, &n.CreatedAt); err != nil {
return nil, err
}
networks = append(networks, &n)
}
if err := rows.Err(); err != nil {
return nil, err
}
for _, n := range networks {
n.MemberHumanIds, err = r.getMemberHumanIds(ctx, n.ID)
if err != nil {
return nil, err
}
}
return networks, nil
}
// Invitation methods
func (r *repositoryImpl) createInvitation(ctx context.Context, networkID, email string) error {
+6
View File
@@ -26,6 +26,8 @@ type Service interface {
RemoveMember(ctx context.Context, networkID, humanId string) error
ListForHuman(ctx context.Context, humanId string) ([]*Network, error)
IsMember(ctx context.Context, networkID, humanId string) (bool, error)
// ListAll returns all networks with their members
ListAll(ctx context.Context) ([]*Network, error)
// Invitations (email-based, for users who haven't registered yet)
InviteByEmail(ctx context.Context, networkID string, emails []string) error
@@ -116,6 +118,10 @@ func (s *serviceImpl) IsMember(ctx context.Context, networkID, humanId string) (
return s.repo.isMember(ctx, networkID, humanId)
}
func (s *serviceImpl) ListAll(ctx context.Context) ([]*Network, error) {
return s.repo.listAll(ctx)
}
// Invitation methods
func (s *serviceImpl) InviteByEmail(ctx context.Context, networkID string, emails []string) error {