| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- package main
- import (
- "encoding/json"
- "net/http"
- "os"
- "strconv"
- "strings"
- "time"
- notification "imuslab.com/arozos/mod/notification"
- "imuslab.com/arozos/mod/notification/agents/webhookn"
- prout "imuslab.com/arozos/mod/prouter"
- "imuslab.com/arozos/mod/utils"
- )
- /*
- Notification Settings
- Registers the "Notifications" tab in System Settings and the endpoints that
- back both the per-user preference UI (any logged-in user) and the
- admin-only agent configuration (Telegram bot token, SMTP credentials).
- User endpoints (login required):
- GET /system/notification/agents – list delivery agents + availability
- GET /system/notification/preference – current user's preferences
- POST /system/notification/preference – save current user's preferences
- GET /system/notification/desktop/list – poll + clear desktop notifications
- POST /system/notification/test – send a test notification to self
- Admin endpoints (admin required):
- GET/POST /system/notification/config/telegram – Telegram bot token
- GET/POST /system/notification/config/smtp – SMTP credentials
- */
- func NotificationSettingInit() {
- //Register the settings tab. It is visible to all users; the admin-only
- //sections are hidden client side and enforced server side.
- registerSetting(settingModule{
- Name: "Notifications",
- Desc: "Choose how you receive notifications and configure delivery agents",
- IconPath: "SystemAO/notification/img/small_icon.svg",
- Group: "Desktop",
- StartDir: "SystemAO/notification/index.html",
- RequireAdmin: false,
- })
- //Router for endpoints available to any logged-in user.
- userRouter := prout.NewModuleRouter(prout.RouterOption{
- ModuleName: "System Setting",
- AdminOnly: false,
- UserHandler: userHandler,
- DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
- utils.SendErrorResponse(w, "Permission Denied")
- },
- })
- userRouter.HandleFunc("/system/notification/agents", handleNotificationAgentList)
- userRouter.HandleFunc("/system/notification/preference", handleNotificationPreference)
- userRouter.HandleFunc("/system/notification/desktop/list", handleDesktopNotificationList)
- userRouter.HandleFunc("/system/notification/test", handleNotificationTest)
- //Admin-only router for agent configuration (holds secrets).
- adminRouter := prout.NewModuleRouter(prout.RouterOption{
- ModuleName: "System Setting",
- AdminOnly: true,
- UserHandler: userHandler,
- DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
- utils.SendErrorResponse(w, "Permission Denied")
- },
- })
- adminRouter.HandleFunc("/system/notification/config/telegram", handleTelegramConfig)
- adminRouter.HandleFunc("/system/notification/config/smtp", handleSMTPConfig)
- }
- // notificationAgentInfo is the JSON shape describing a delivery agent for the UI.
- type notificationAgentInfo struct {
- Name string `json:"name"`
- Desc string `json:"desc"`
- Available bool `json:"available"` //Whether the agent can deliver at all (admin-level config present)
- RequiresUserSetup bool `json:"requiresUserSetup"` //Whether the user must provide per-user info (chat id / webhook)
- }
- func handleNotificationAgentList(w http.ResponseWriter, r *http.Request) {
- if notificationQueue == nil {
- utils.SendErrorResponse(w, "Notification system not ready")
- return
- }
- agents := []notificationAgentInfo{}
- for _, name := range notificationQueue.ListConsumerAgentNames() {
- agent := notificationQueue.GetAgentByName(name)
- if agent == nil {
- continue
- }
- info := notificationAgentInfo{
- Name: name,
- Desc: agent.Desc(),
- Available: true,
- }
- switch name {
- case "telegram":
- info.Available = telegramNotificationAgent != nil && telegramNotificationAgent.IsConfigured()
- info.RequiresUserSetup = true
- case "smtpn":
- info.Available = smtpConfigured()
- case "webhook":
- info.RequiresUserSetup = true
- }
- agents = append(agents, info)
- }
- js, _ := json.Marshal(agents)
- utils.SendJSONResponse(w, string(js))
- }
- func handleNotificationPreference(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- utils.SendErrorResponse(w, "User not logged in")
- return
- }
- if notificationPreferenceStore == nil {
- utils.SendErrorResponse(w, "Notification preference store not ready")
- return
- }
- if r.Method == http.MethodGet {
- pref := notificationPreferenceStore.Get(userinfo.Username)
- js, _ := json.Marshal(pref)
- utils.SendJSONResponse(w, string(js))
- return
- }
- //POST: save preference
- pref := notificationPreferenceStore.Get(userinfo.Username)
- //The delivery matrix: agent name -> priority label ("low"/"medium"/"high")
- //-> enabled. Sent as a JSON object in the "channels" parameter.
- if channels, err := utils.PostPara(r, "channels"); err == nil {
- parsed := map[string]map[string]bool{}
- if jsonErr := json.Unmarshal([]byte(channels), &parsed); jsonErr == nil {
- pref.Channels = parsed
- } else {
- utils.SendErrorResponse(w, "Invalid channels matrix")
- return
- }
- }
- if chatID, err := utils.PostPara(r, "telegramChatID"); err == nil {
- pref.TelegramChatID = strings.TrimSpace(chatID)
- }
- if webhookURL, err := utils.PostPara(r, "webhookURL"); err == nil {
- pref.Webhook.URL = strings.TrimSpace(webhookURL)
- }
- if webhookMethod, err := utils.PostPara(r, "webhookMethod"); err == nil {
- pref.Webhook.Method = strings.TrimSpace(webhookMethod)
- }
- if webhookContentType, err := utils.PostPara(r, "webhookContentType"); err == nil {
- pref.Webhook.ContentType = strings.TrimSpace(webhookContentType)
- }
- if webhookBody, err := utils.PostPara(r, "webhookBody"); err == nil {
- pref.Webhook.BodyTemplate = webhookBody
- }
- //Reject an invalid webhook URL early so the user gets feedback.
- if pref.Webhook.URL != "" {
- if err := webhookn.ValidateTarget(pref.Webhook); err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- }
- if err := notificationPreferenceStore.Set(userinfo.Username, pref); err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- utils.SendOK(w)
- }
- func handleDesktopNotificationList(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- utils.SendErrorResponse(w, "User not logged in")
- return
- }
- if desktopNotificationAgent == nil {
- utils.SendJSONResponse(w, "[]")
- return
- }
- items := desktopNotificationAgent.PollNotifications(userinfo.Username)
- js, _ := json.Marshal(items)
- utils.SendJSONResponse(w, string(js))
- }
- func handleNotificationTest(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- utils.SendErrorResponse(w, "User not logged in")
- return
- }
- priority := notification.PriorityMedium
- if p, err := utils.PostPara(r, "priority"); err == nil {
- priority = parsePriority(p)
- }
- payload := ¬ification.NotificationPayload{
- ID: strconv.FormatInt(time.Now().UnixNano(), 10),
- Title: "Test Notification",
- Message: "This is a test notification from your ArozOS notification settings.",
- Receiver: []string{userinfo.Username},
- Sender: "Notification Settings",
- Priority: priority,
- Timestamp: time.Now().Unix(),
- }
- //An explicit agent may be requested for a targeted test.
- if agent, err := utils.PostPara(r, "agent"); err == nil && agent != "" {
- payload.ReciverAgents = []string{agent}
- }
- if err := sendUserNotification(payload); err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- utils.SendOK(w)
- }
- // ─── Admin agent configuration ───────────────────────────────────────────────
- type telegramConfigView struct {
- //BotTokenSet reports whether a token exists without leaking it. On POST the
- //caller supplies a new token in the "botToken" parameter.
- BotTokenSet bool `json:"botTokenSet"`
- }
- func handleTelegramConfig(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodGet {
- view := telegramConfigView{
- BotTokenSet: telegramNotificationAgent != nil && telegramNotificationAgent.IsConfigured(),
- }
- js, _ := json.Marshal(view)
- utils.SendJSONResponse(w, string(js))
- return
- }
- //POST: update the bot token
- botToken, err := utils.PostPara(r, "botToken")
- if err != nil {
- utils.SendErrorResponse(w, "botToken is required")
- return
- }
- botToken = strings.TrimSpace(botToken)
- //Persist to disk (config file only holds the bot token).
- conf := map[string]string{"BotToken": botToken}
- js, _ := json.MarshalIndent(conf, "", " ")
- if err := os.WriteFile(telegramConfigPath, js, 0775); err != nil {
- utils.SendErrorResponse(w, "Unable to save Telegram config: "+err.Error())
- return
- }
- //Apply live so no restart is required.
- if telegramNotificationAgent != nil {
- telegramNotificationAgent.BotToken = botToken
- }
- utils.SendOK(w)
- }
- // smtpConfigView mirrors the persisted SMTP config with the password masked on
- // read.
- type smtpConfigView struct {
- SMTPSenderDisplayName string `json:"SMTPSenderDisplayName"`
- SMTPSender string `json:"SMTPSender"`
- SMTPDomain string `json:"SMTPDomain"`
- SMTPPort int `json:"SMTPPort"`
- PasswordSet bool `json:"PasswordSet"`
- }
- type smtpConfigFile struct {
- SMTPSenderDisplayName string
- SMTPSender string
- SMTPPassword string
- SMTPDomain string
- SMTPPort int
- }
- func handleSMTPConfig(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodGet {
- conf := readSMTPConfig()
- view := smtpConfigView{
- SMTPSenderDisplayName: conf.SMTPSenderDisplayName,
- SMTPSender: conf.SMTPSender,
- SMTPDomain: conf.SMTPDomain,
- SMTPPort: conf.SMTPPort,
- PasswordSet: conf.SMTPPassword != "",
- }
- js, _ := json.Marshal(view)
- utils.SendJSONResponse(w, string(js))
- return
- }
- //POST: update SMTP config. An empty password keeps the existing one.
- conf := readSMTPConfig()
- if v, err := utils.PostPara(r, "senderDisplayName"); err == nil {
- conf.SMTPSenderDisplayName = v
- }
- if v, err := utils.PostPara(r, "sender"); err == nil {
- conf.SMTPSender = strings.TrimSpace(v)
- }
- if v, err := utils.PostPara(r, "domain"); err == nil {
- conf.SMTPDomain = strings.TrimSpace(v)
- }
- if v, err := utils.PostPara(r, "port"); err == nil {
- if p, convErr := strconv.Atoi(strings.TrimSpace(v)); convErr == nil {
- conf.SMTPPort = p
- }
- }
- if v, err := utils.PostPara(r, "password"); err == nil && strings.TrimSpace(v) != "" {
- conf.SMTPPassword = v
- }
- js, _ := json.MarshalIndent(conf, "", " ")
- if err := os.WriteFile(smtpnConfigPath, js, 0775); err != nil {
- utils.SendErrorResponse(w, "Unable to save SMTP config: "+err.Error())
- return
- }
- //Apply live to the running agent.
- if smtpNotificationAgent != nil {
- smtpNotificationAgent.SMTPSenderDisplayName = conf.SMTPSenderDisplayName
- smtpNotificationAgent.SMTPSender = conf.SMTPSender
- smtpNotificationAgent.SMTPPassword = conf.SMTPPassword
- smtpNotificationAgent.SMTPDomain = conf.SMTPDomain
- smtpNotificationAgent.SMTPPort = conf.SMTPPort
- }
- utils.SendOK(w)
- }
- // ─── helpers ────────────────────────────────────────────────────────────────
- // parsePriority accepts either a numeric or textual priority.
- func parsePriority(raw string) int {
- raw = strings.TrimSpace(raw)
- if n, err := strconv.Atoi(raw); err == nil {
- return notification.NormalizePriority(n)
- }
- return notification.PriorityFromString(raw)
- }
- func readSMTPConfig() smtpConfigFile {
- conf := smtpConfigFile{}
- data, err := os.ReadFile(smtpnConfigPath)
- if err != nil {
- return conf
- }
- json.Unmarshal(data, &conf)
- return conf
- }
- // smtpConfigured reports whether the SMTP agent has enough config to send mail.
- func smtpConfigured() bool {
- conf := readSMTPConfig()
- return conf.SMTPSender != "" && conf.SMTPDomain != "" && conf.SMTPPort != 0
- }
|