adds email notifications
This commit is contained in:
12
db/main.go
12
db/main.go
@@ -114,7 +114,7 @@ func createTrailHandler(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
||||
if e.Record.GetBool("public") {
|
||||
notification := util.Notification{
|
||||
Type: util.TrailCreate,
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]string{
|
||||
"id": e.Record.Id,
|
||||
"trail": e.Record.GetString("name"),
|
||||
},
|
||||
@@ -163,7 +163,7 @@ func createTrailShareHandler(app *pocketbase.PocketBase, client meilisearch.Serv
|
||||
|
||||
notification := util.Notification{
|
||||
Type: util.TrailShare,
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]string{
|
||||
"id": shareTrail.Id,
|
||||
"trail": shareTrail.GetString("name"),
|
||||
"author": shareTrailAuthor.GetString("username"),
|
||||
@@ -185,7 +185,7 @@ func createListShareHandler(app *pocketbase.PocketBase) func(e *core.RecordCreat
|
||||
|
||||
notification := util.Notification{
|
||||
Type: util.ListShare,
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]string{
|
||||
"id": shareList.Id,
|
||||
"list": shareList.GetString("name"),
|
||||
"author": shareListAuthor.GetString("username"),
|
||||
@@ -211,7 +211,7 @@ func createListHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateEven
|
||||
}
|
||||
notification := util.Notification{
|
||||
Type: util.ListCreate,
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]string{
|
||||
"id": e.Record.Id,
|
||||
"list": e.Record.GetString("name"),
|
||||
},
|
||||
@@ -231,7 +231,7 @@ func createFollowHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateEv
|
||||
|
||||
notification := util.Notification{
|
||||
Type: util.NewFollower,
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]string{
|
||||
"follower": follower.GetString("username"),
|
||||
},
|
||||
Seen: false,
|
||||
@@ -252,7 +252,7 @@ func createCommentHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateE
|
||||
|
||||
notification := util.Notification{
|
||||
Type: util.TrailComment,
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]string{
|
||||
"id": commentTrail.Id,
|
||||
"author": commentAuthor.GetString("username"),
|
||||
"trail": commentTrail.GetString("name"),
|
||||
|
||||
@@ -86,7 +86,7 @@ func init() {
|
||||
|
||||
return daos.New(db).SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
|
||||
76
db/migrations/1735935107_updated_lists.go
Normal file
76
db/migrations/1735935107_updated_lists.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("r6gu2ajyidy1x69")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
edit_author := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "kwm6zdet",
|
||||
"name": "author",
|
||||
"type": "relation",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
}`), edit_author); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(edit_author)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("r6gu2ajyidy1x69")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
edit_author := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "kwm6zdet",
|
||||
"name": "author",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
}`), edit_author); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(edit_author)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
122
db/templates/mail/notification.html
Normal file
122
db/templates/mail/notification.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.email-container {
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #242734;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.header img {
|
||||
max-width: 150px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.greeting,
|
||||
.goodbye {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 20px 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
background: #f1f1f1;
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #242734;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #777777;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #242734;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<!-- Header Section -->
|
||||
<div class="header">
|
||||
<img src="{{.AppUrl}}/imgs/logo_text_light.png" alt="wanderer" />
|
||||
</div>
|
||||
<!-- Greeting -->
|
||||
<div class="greeting">
|
||||
<p>Hello {{.RecipientName}},</p>
|
||||
<p>You have a new notification:</p>
|
||||
</div>
|
||||
<!-- Dynamic Content -->
|
||||
<div class="content">
|
||||
{{.Content}}
|
||||
<div class="">
|
||||
<a class="button" href="{{.AppUrl}}">
|
||||
Check it out!
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="goodbye">
|
||||
<p>Kind regards,</p>
|
||||
<p>Your wanderer team</p>
|
||||
</div>
|
||||
<!-- Footer Section -->
|
||||
<div class="footer">
|
||||
<p>© 2025 wanderer | <a href="{{.AppUrl}}/settings/notifications">Unsubscribe</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
67
db/util/email_templates.go
Normal file
67
db/util/email_templates.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
t "html/template"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/template"
|
||||
)
|
||||
|
||||
type EmailData struct {
|
||||
AppUrl string
|
||||
RecipientName string
|
||||
Content t.HTML
|
||||
Link string
|
||||
FollowerName string
|
||||
}
|
||||
|
||||
var notificationTemplates = map[NotificationType]string{
|
||||
TrailCreate: "{{.Author}} has created a new trail: {{.trail}}.",
|
||||
TrailShare: "{{.Author}} has shared a trail with you: {{.trail}}.",
|
||||
ListCreate: "{{.Author}} has created a new list: {{.list}}.",
|
||||
ListShare: "{{.Author}} has shared a list with you: {{.list}}.",
|
||||
NewFollower: "Good news! You have a new follower: {{.Author}}.",
|
||||
TrailComment: "{{.Author}} commented on your trail '{{.trail}}': '{{.comment}}'.",
|
||||
}
|
||||
|
||||
func GenerateHTML(appUrl string, recipientName string, authorName string, notificationType NotificationType, metadata map[string]string) (string, error) {
|
||||
registry := template.NewRegistry()
|
||||
|
||||
// Get custom content template for the notification type
|
||||
customTemplate, exists := notificationTemplates[notificationType]
|
||||
if !exists {
|
||||
return "", errors.New("unknown notification type")
|
||||
}
|
||||
|
||||
// Create content template with dynamic data
|
||||
contentTmpl, err := t.New("content").Parse(customTemplate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Render the custom content with data
|
||||
metadata["Author"] = authorName
|
||||
var contentBuffer bytes.Buffer
|
||||
err = contentTmpl.Execute(&contentBuffer, metadata)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Assemble the final email with boilerplate and custom content
|
||||
content := EmailData{
|
||||
AppUrl: appUrl,
|
||||
RecipientName: recipientName,
|
||||
Content: t.HTML(contentBuffer.String()),
|
||||
}
|
||||
|
||||
html, err := registry.LoadFiles(
|
||||
"templates/mail/notification.html",
|
||||
).Render(content)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return html, nil
|
||||
}
|
||||
@@ -2,10 +2,12 @@ package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
)
|
||||
|
||||
type NotificationType string
|
||||
@@ -20,10 +22,10 @@ const (
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
Type NotificationType `json:"type"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
Seen bool `json:"seen"`
|
||||
Author string `json:"author"`
|
||||
Type NotificationType `json:"type"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Seen bool `json:"seen"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
type NotificationSettings struct {
|
||||
@@ -59,22 +61,50 @@ func SendNotification(app *pocketbase.PocketBase, notification Notification, rec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !permissions.Web {
|
||||
return nil
|
||||
}
|
||||
|
||||
notifications, err := app.Dao().FindCollectionByNameOrId("notifications")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n := models.NewRecord(notifications)
|
||||
n.Set("type", string(notification.Type))
|
||||
n.Set("metadata", notification.Metadata)
|
||||
n.Set("seen", notification.Seen)
|
||||
n.Set("recipient", recipient)
|
||||
n.Set("author", notification.Author)
|
||||
|
||||
if err := app.Dao().SaveRecord(n); err != nil {
|
||||
return err
|
||||
if permissions.Web {
|
||||
n := models.NewRecord(notifications)
|
||||
n.Set("type", string(notification.Type))
|
||||
n.Set("metadata", notification.Metadata)
|
||||
n.Set("seen", notification.Seen)
|
||||
n.Set("recipient", recipient)
|
||||
n.Set("author", notification.Author)
|
||||
|
||||
if err := app.Dao().SaveRecord(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if permissions.Email {
|
||||
recipientUser, err := app.Dao().FindRecordById("users", recipient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorUser, err := app.Dao().FindRecordById("users", notification.Author)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html, err := GenerateHTML(app.Settings().Meta.AppUrl, recipientUser.Username(), authorUser.Username(), notification.Type, notification.Metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := &mailer.Message{
|
||||
From: mail.Address{
|
||||
Address: app.Settings().Meta.SenderAddress,
|
||||
Name: app.Settings().Meta.SenderName,
|
||||
},
|
||||
To: []mail.Address{{Address: recipientUser.Email()}},
|
||||
Subject: "wanderer - New Notification",
|
||||
HTML: html,
|
||||
}
|
||||
|
||||
app.NewMailClient().Send(message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -85,32 +115,11 @@ func SendNotificationToFollowers(app *pocketbase.PocketBase, notification Notifi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notifications, err := app.Dao().FindCollectionByNameOrId("notifications")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range followers {
|
||||
recipient := f.GetString("follower")
|
||||
if notification.Author == recipient {
|
||||
continue
|
||||
}
|
||||
permissions, err := getNotificationPermissions(app, recipient, notification.Type)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !permissions.Web {
|
||||
continue
|
||||
}
|
||||
n := models.NewRecord(notifications)
|
||||
n.Set("type", string(notification.Type))
|
||||
n.Set("metadata", notification.Metadata)
|
||||
n.Set("seen", notification.Seen)
|
||||
n.Set("recipient", recipient)
|
||||
n.Set("author", notification.Author)
|
||||
SendNotification(app, notification, recipient)
|
||||
|
||||
if err := app.Dao().SaveRecord(n); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user