notification_settings.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. package main
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "os"
  6. "strconv"
  7. "strings"
  8. "time"
  9. notification "imuslab.com/arozos/mod/notification"
  10. "imuslab.com/arozos/mod/notification/agents/webhookn"
  11. prout "imuslab.com/arozos/mod/prouter"
  12. "imuslab.com/arozos/mod/utils"
  13. )
  14. /*
  15. Notification Settings
  16. Registers the "Notifications" tab in System Settings and the endpoints that
  17. back both the per-user preference UI (any logged-in user) and the
  18. admin-only agent configuration (Telegram bot token, SMTP credentials).
  19. User endpoints (login required):
  20. GET /system/notification/agents – list delivery agents + availability
  21. GET /system/notification/preference – current user's preferences
  22. POST /system/notification/preference – save current user's preferences
  23. GET /system/notification/desktop/list – poll + clear desktop notifications
  24. POST /system/notification/test – send a test notification to self
  25. Admin endpoints (admin required):
  26. GET/POST /system/notification/config/telegram – Telegram bot token
  27. GET/POST /system/notification/config/smtp – SMTP credentials
  28. */
  29. func NotificationSettingInit() {
  30. //Register the settings tab. It is visible to all users; the admin-only
  31. //sections are hidden client side and enforced server side.
  32. registerSetting(settingModule{
  33. Name: "Notifications",
  34. Desc: "Choose how you receive notifications and configure delivery agents",
  35. IconPath: "SystemAO/notification/img/small_icon.svg",
  36. Group: "Desktop",
  37. StartDir: "SystemAO/notification/index.html",
  38. RequireAdmin: false,
  39. })
  40. //Router for endpoints available to any logged-in user.
  41. userRouter := prout.NewModuleRouter(prout.RouterOption{
  42. ModuleName: "System Setting",
  43. AdminOnly: false,
  44. UserHandler: userHandler,
  45. DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
  46. utils.SendErrorResponse(w, "Permission Denied")
  47. },
  48. })
  49. userRouter.HandleFunc("/system/notification/agents", handleNotificationAgentList)
  50. userRouter.HandleFunc("/system/notification/preference", handleNotificationPreference)
  51. userRouter.HandleFunc("/system/notification/desktop/list", handleDesktopNotificationList)
  52. userRouter.HandleFunc("/system/notification/test", handleNotificationTest)
  53. //Admin-only router for agent configuration (holds secrets).
  54. adminRouter := prout.NewModuleRouter(prout.RouterOption{
  55. ModuleName: "System Setting",
  56. AdminOnly: true,
  57. UserHandler: userHandler,
  58. DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
  59. utils.SendErrorResponse(w, "Permission Denied")
  60. },
  61. })
  62. adminRouter.HandleFunc("/system/notification/config/telegram", handleTelegramConfig)
  63. adminRouter.HandleFunc("/system/notification/config/smtp", handleSMTPConfig)
  64. }
  65. // notificationAgentInfo is the JSON shape describing a delivery agent for the UI.
  66. type notificationAgentInfo struct {
  67. Name string `json:"name"`
  68. Desc string `json:"desc"`
  69. Available bool `json:"available"` //Whether the agent can deliver at all (admin-level config present)
  70. RequiresUserSetup bool `json:"requiresUserSetup"` //Whether the user must provide per-user info (chat id / webhook)
  71. }
  72. func handleNotificationAgentList(w http.ResponseWriter, r *http.Request) {
  73. if notificationQueue == nil {
  74. utils.SendErrorResponse(w, "Notification system not ready")
  75. return
  76. }
  77. agents := []notificationAgentInfo{}
  78. for _, name := range notificationQueue.ListConsumerAgentNames() {
  79. agent := notificationQueue.GetAgentByName(name)
  80. if agent == nil {
  81. continue
  82. }
  83. info := notificationAgentInfo{
  84. Name: name,
  85. Desc: agent.Desc(),
  86. Available: true,
  87. }
  88. switch name {
  89. case "telegram":
  90. info.Available = telegramNotificationAgent != nil && telegramNotificationAgent.IsConfigured()
  91. info.RequiresUserSetup = true
  92. case "smtpn":
  93. info.Available = smtpConfigured()
  94. case "webhook":
  95. info.RequiresUserSetup = true
  96. }
  97. agents = append(agents, info)
  98. }
  99. js, _ := json.Marshal(agents)
  100. utils.SendJSONResponse(w, string(js))
  101. }
  102. func handleNotificationPreference(w http.ResponseWriter, r *http.Request) {
  103. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  104. if err != nil {
  105. utils.SendErrorResponse(w, "User not logged in")
  106. return
  107. }
  108. if notificationPreferenceStore == nil {
  109. utils.SendErrorResponse(w, "Notification preference store not ready")
  110. return
  111. }
  112. if r.Method == http.MethodGet {
  113. pref := notificationPreferenceStore.Get(userinfo.Username)
  114. js, _ := json.Marshal(pref)
  115. utils.SendJSONResponse(w, string(js))
  116. return
  117. }
  118. //POST: save preference
  119. pref := notificationPreferenceStore.Get(userinfo.Username)
  120. //The delivery matrix: agent name -> priority label ("low"/"medium"/"high")
  121. //-> enabled. Sent as a JSON object in the "channels" parameter.
  122. if channels, err := utils.PostPara(r, "channels"); err == nil {
  123. parsed := map[string]map[string]bool{}
  124. if jsonErr := json.Unmarshal([]byte(channels), &parsed); jsonErr == nil {
  125. pref.Channels = parsed
  126. } else {
  127. utils.SendErrorResponse(w, "Invalid channels matrix")
  128. return
  129. }
  130. }
  131. if chatID, err := utils.PostPara(r, "telegramChatID"); err == nil {
  132. pref.TelegramChatID = strings.TrimSpace(chatID)
  133. }
  134. if webhookURL, err := utils.PostPara(r, "webhookURL"); err == nil {
  135. pref.Webhook.URL = strings.TrimSpace(webhookURL)
  136. }
  137. if webhookMethod, err := utils.PostPara(r, "webhookMethod"); err == nil {
  138. pref.Webhook.Method = strings.TrimSpace(webhookMethod)
  139. }
  140. if webhookContentType, err := utils.PostPara(r, "webhookContentType"); err == nil {
  141. pref.Webhook.ContentType = strings.TrimSpace(webhookContentType)
  142. }
  143. if webhookBody, err := utils.PostPara(r, "webhookBody"); err == nil {
  144. pref.Webhook.BodyTemplate = webhookBody
  145. }
  146. //Reject an invalid webhook URL early so the user gets feedback.
  147. if pref.Webhook.URL != "" {
  148. if err := webhookn.ValidateTarget(pref.Webhook); err != nil {
  149. utils.SendErrorResponse(w, err.Error())
  150. return
  151. }
  152. }
  153. if err := notificationPreferenceStore.Set(userinfo.Username, pref); err != nil {
  154. utils.SendErrorResponse(w, err.Error())
  155. return
  156. }
  157. utils.SendOK(w)
  158. }
  159. func handleDesktopNotificationList(w http.ResponseWriter, r *http.Request) {
  160. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  161. if err != nil {
  162. utils.SendErrorResponse(w, "User not logged in")
  163. return
  164. }
  165. if desktopNotificationAgent == nil {
  166. utils.SendJSONResponse(w, "[]")
  167. return
  168. }
  169. items := desktopNotificationAgent.PollNotifications(userinfo.Username)
  170. js, _ := json.Marshal(items)
  171. utils.SendJSONResponse(w, string(js))
  172. }
  173. func handleNotificationTest(w http.ResponseWriter, r *http.Request) {
  174. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  175. if err != nil {
  176. utils.SendErrorResponse(w, "User not logged in")
  177. return
  178. }
  179. priority := notification.PriorityMedium
  180. if p, err := utils.PostPara(r, "priority"); err == nil {
  181. priority = parsePriority(p)
  182. }
  183. payload := &notification.NotificationPayload{
  184. ID: strconv.FormatInt(time.Now().UnixNano(), 10),
  185. Title: "Test Notification",
  186. Message: "This is a test notification from your ArozOS notification settings.",
  187. Receiver: []string{userinfo.Username},
  188. Sender: "Notification Settings",
  189. Priority: priority,
  190. Timestamp: time.Now().Unix(),
  191. }
  192. //An explicit agent may be requested for a targeted test.
  193. if agent, err := utils.PostPara(r, "agent"); err == nil && agent != "" {
  194. payload.ReciverAgents = []string{agent}
  195. }
  196. if err := sendUserNotification(payload); err != nil {
  197. utils.SendErrorResponse(w, err.Error())
  198. return
  199. }
  200. utils.SendOK(w)
  201. }
  202. // ─── Admin agent configuration ───────────────────────────────────────────────
  203. type telegramConfigView struct {
  204. //BotTokenSet reports whether a token exists without leaking it. On POST the
  205. //caller supplies a new token in the "botToken" parameter.
  206. BotTokenSet bool `json:"botTokenSet"`
  207. }
  208. func handleTelegramConfig(w http.ResponseWriter, r *http.Request) {
  209. if r.Method == http.MethodGet {
  210. view := telegramConfigView{
  211. BotTokenSet: telegramNotificationAgent != nil && telegramNotificationAgent.IsConfigured(),
  212. }
  213. js, _ := json.Marshal(view)
  214. utils.SendJSONResponse(w, string(js))
  215. return
  216. }
  217. //POST: update the bot token
  218. botToken, err := utils.PostPara(r, "botToken")
  219. if err != nil {
  220. utils.SendErrorResponse(w, "botToken is required")
  221. return
  222. }
  223. botToken = strings.TrimSpace(botToken)
  224. //Persist to disk (config file only holds the bot token).
  225. conf := map[string]string{"BotToken": botToken}
  226. js, _ := json.MarshalIndent(conf, "", " ")
  227. if err := os.WriteFile(telegramConfigPath, js, 0775); err != nil {
  228. utils.SendErrorResponse(w, "Unable to save Telegram config: "+err.Error())
  229. return
  230. }
  231. //Apply live so no restart is required.
  232. if telegramNotificationAgent != nil {
  233. telegramNotificationAgent.BotToken = botToken
  234. }
  235. utils.SendOK(w)
  236. }
  237. // smtpConfigView mirrors the persisted SMTP config with the password masked on
  238. // read.
  239. type smtpConfigView struct {
  240. SMTPSenderDisplayName string `json:"SMTPSenderDisplayName"`
  241. SMTPSender string `json:"SMTPSender"`
  242. SMTPDomain string `json:"SMTPDomain"`
  243. SMTPPort int `json:"SMTPPort"`
  244. PasswordSet bool `json:"PasswordSet"`
  245. }
  246. type smtpConfigFile struct {
  247. SMTPSenderDisplayName string
  248. SMTPSender string
  249. SMTPPassword string
  250. SMTPDomain string
  251. SMTPPort int
  252. }
  253. func handleSMTPConfig(w http.ResponseWriter, r *http.Request) {
  254. if r.Method == http.MethodGet {
  255. conf := readSMTPConfig()
  256. view := smtpConfigView{
  257. SMTPSenderDisplayName: conf.SMTPSenderDisplayName,
  258. SMTPSender: conf.SMTPSender,
  259. SMTPDomain: conf.SMTPDomain,
  260. SMTPPort: conf.SMTPPort,
  261. PasswordSet: conf.SMTPPassword != "",
  262. }
  263. js, _ := json.Marshal(view)
  264. utils.SendJSONResponse(w, string(js))
  265. return
  266. }
  267. //POST: update SMTP config. An empty password keeps the existing one.
  268. conf := readSMTPConfig()
  269. if v, err := utils.PostPara(r, "senderDisplayName"); err == nil {
  270. conf.SMTPSenderDisplayName = v
  271. }
  272. if v, err := utils.PostPara(r, "sender"); err == nil {
  273. conf.SMTPSender = strings.TrimSpace(v)
  274. }
  275. if v, err := utils.PostPara(r, "domain"); err == nil {
  276. conf.SMTPDomain = strings.TrimSpace(v)
  277. }
  278. if v, err := utils.PostPara(r, "port"); err == nil {
  279. if p, convErr := strconv.Atoi(strings.TrimSpace(v)); convErr == nil {
  280. conf.SMTPPort = p
  281. }
  282. }
  283. if v, err := utils.PostPara(r, "password"); err == nil && strings.TrimSpace(v) != "" {
  284. conf.SMTPPassword = v
  285. }
  286. js, _ := json.MarshalIndent(conf, "", " ")
  287. if err := os.WriteFile(smtpnConfigPath, js, 0775); err != nil {
  288. utils.SendErrorResponse(w, "Unable to save SMTP config: "+err.Error())
  289. return
  290. }
  291. //Apply live to the running agent.
  292. if smtpNotificationAgent != nil {
  293. smtpNotificationAgent.SMTPSenderDisplayName = conf.SMTPSenderDisplayName
  294. smtpNotificationAgent.SMTPSender = conf.SMTPSender
  295. smtpNotificationAgent.SMTPPassword = conf.SMTPPassword
  296. smtpNotificationAgent.SMTPDomain = conf.SMTPDomain
  297. smtpNotificationAgent.SMTPPort = conf.SMTPPort
  298. }
  299. utils.SendOK(w)
  300. }
  301. // ─── helpers ────────────────────────────────────────────────────────────────
  302. // parsePriority accepts either a numeric or textual priority.
  303. func parsePriority(raw string) int {
  304. raw = strings.TrimSpace(raw)
  305. if n, err := strconv.Atoi(raw); err == nil {
  306. return notification.NormalizePriority(n)
  307. }
  308. return notification.PriorityFromString(raw)
  309. }
  310. func readSMTPConfig() smtpConfigFile {
  311. conf := smtpConfigFile{}
  312. data, err := os.ReadFile(smtpnConfigPath)
  313. if err != nil {
  314. return conf
  315. }
  316. json.Unmarshal(data, &conf)
  317. return conf
  318. }
  319. // smtpConfigured reports whether the SMTP agent has enough config to send mail.
  320. func smtpConfigured() bool {
  321. conf := readSMTPConfig()
  322. return conf.SMTPSender != "" && conf.SMTPDomain != "" && conf.SMTPPort != 0
  323. }