feat: add ntfy push notification for update checker
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s

Update notifications now go through three channels:
- Admin dashboard banner (existing)
- Email to admin (added in previous commit)
- ntfy push notification (new)

Configure in app.ini:
  [ntfy]
  ENABLED = true
  SERVER_URL = https://ntfy.mokoconsulting.tech
  DEFAULT_TOPIC = mokogitea
  TOKEN = (optional bearer token)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-05-25 18:34:14 -05:00
parent 13352e7213
commit 49fe3cf6eb
+56 -16
View File
@@ -4,8 +4,11 @@
package mailer
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
@@ -13,19 +16,8 @@ import (
sender_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mailer/sender"
)
// SendUpdateNotification emails all admin users when a new MokoGitea version is available.
// SendUpdateNotification emails the admin and sends ntfy push when a new MokoGitea version is available.
func SendUpdateNotification(version, channel, releaseURL, dockerImage string) {
if setting.MailService == nil {
log.Debug("Update notification skipped: mail service not configured")
return
}
admin, err := user_model.GetAdminUser(context.Background())
if err != nil {
log.Error("SendUpdateNotification: GetAdminUser: %v", err)
return
}
subject := fmt.Sprintf("[MokoGitea] Update available: %s (%s)", version, channel)
body := fmt.Sprintf(`MokoGitea Update Available
@@ -45,9 +37,57 @@ Current: %s`, channel, version, channel, setting.AppVer)
body += fmt.Sprintf("\n\nUpdate the channel in Site Administration > Dashboard.\n\n— %s", setting.AppName)
msg := sender_service.NewMessage(admin.EmailTo(), subject, body)
msg.Info = "Update notification"
// Send email to admin
if setting.MailService != nil {
admin, err := user_model.GetAdminUser(context.Background())
if err != nil {
log.Error("SendUpdateNotification: GetAdminUser: %v", err)
} else {
msg := sender_service.NewMessage(admin.EmailTo(), subject, body)
msg.Info = "Update notification"
SendAsync(msg)
log.Info("Update email sent to %s for version %s [%s]", admin.Email, version, channel)
}
}
SendAsync(msg)
log.Info("Update notification sent to %s for version %s [%s]", admin.Email, version, channel)
// Send ntfy push notification
if setting.Ntfy.Enabled && setting.Ntfy.ServerURL != "" {
sendNtfyNotification(subject, body, releaseURL)
}
}
func sendNtfyNotification(title, body, clickURL string) {
url := fmt.Sprintf("%s/%s", setting.Ntfy.ServerURL, setting.Ntfy.DefaultTopic)
req, err := http.NewRequest("POST", url, bytes.NewBufferString(body))
if err != nil {
log.Error("ntfy: create request: %v", err)
return
}
req.Header.Set("Title", title)
req.Header.Set("Priority", "high")
req.Header.Set("Tags", "arrow_up,mokogitea")
if clickURL != "" {
req.Header.Set("Click", clickURL)
}
if setting.Ntfy.Token != "" {
req.Header.Set("Authorization", "Bearer "+setting.Ntfy.Token)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error("ntfy: send notification: %v", err)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= 300 {
log.Error("ntfy: unexpected status %d", resp.StatusCode)
return
}
log.Info("ntfy: update notification sent to %s/%s", setting.Ntfy.ServerURL, setting.Ntfy.DefaultTopic)
}