| 1 | import { eq } from "drizzle-orm"; |
| 2 | import { db } |
| 3 | import { settings } |
| 4 | import { user as |
| 5 | import { env } |
| 6 | import { safeDecrypt } |
| 7 | |
| 8 | export { SENTINEL } |
| 9 | |
| 10 | const TIMEOUT_MS = 8000 |
| 11 | |
| 12 | export function isValidDiscordWebhookUrl(raw |
| 13 | let u: |
| 14 | try { |
| 15 | u = new |
| 16 | } catch { |
| 17 | return false; |
| 18 | } |
| 19 | return ( |
| 20 | u.protocol === |
| 21 | (u.hostname === |
| 22 | u.pathname.startsWith |
| 23 | ); |
| 24 | } |
| 25 | |
| 26 | export function isValidTelegramBotToken(token |
| 27 | return /^\d{6,}: |
| 28 | } |
| 29 | |
| 30 | export function isValidTelegramChatId(id |
| 31 | return /^-?\d |
| 32 | } |
| 33 | |
| 34 | export async function postDiscord |
| 35 | if (!isValidDiscordWebhookUrl(url |
| 36 | const res = |
| 37 | method: "POST", |
| 38 | headers: { "Content-Type": |
| 39 | body: JSON.stringify |
| 40 | signal: AbortSignal.timeout |
| 41 | }); |
| 42 | if (!res.ok |
| 43 | } |
| 44 | |
| 45 | function withComponentsParam(url: |
| 46 | return url.includes |
| 47 | } |
| 48 | |
| 49 | export async function postTelegram |
| 50 | if (!isValidTelegramBotToken(token |
| 51 | throw new Error( |
| 52 | } |
| 53 | const res = |
| 54 | method: "POST", |
| 55 | headers: { "Content-Type": |
| 56 | body: JSON.stringify |
| 57 | signal: AbortSignal.timeout |
| 58 | }); |
| 59 | if (!res.ok |
| 60 | } |
| 61 | |
| 62 | async function readSettingsMap() |
| 63 | const rows = |
| 64 | return Object.fromEntries |
| 65 | } |
| 66 | |
| 67 | export async function getAdminNotificationConfig |
| 68 | const s = |
| 69 | return { |
| 70 | appName: s.app_name |
| 71 | appUrl: env.APP_URL |
| 72 | discordEnabled: s.notifications_discord_enabled |
| 73 | discordWebhookUrl: s.notifications_discord_webhook_url |
| 74 | telegramEnabled: s.notifications_telegram_enabled |
| 75 | telegramToken: s.notifications_telegram_bot_token |
| 76 | telegramChatId: s.notifications_telegram_chat_id |
| 77 | }; |
| 78 | } |
| 79 | |
| 80 | export async function getUserNotificationConfig |
| 81 | const [s, |
| 82 | readSettingsMap(), |
| 83 | db.query.user |
| 84 | ]); |
| 85 | if (s.notifications_user_config_enabled |
| 86 | return { |
| 87 | appName: s.app_name |
| 88 | appUrl: env.APP_URL |
| 89 | discordWebhookUrl: owner.notificationDiscordWebhook |
| 90 | telegramToken: owner.notificationTelegramToken |
| 91 | telegramChatId: owner.notificationTelegramChatId |
| 92 | }; |
| 93 | } |
| 94 | |
| 95 | export function notifyAdmins(kind |
| 96 | void (async () |
| 97 | try { |
| 98 | const cfg = |
| 99 | const message = |
| 100 | const components = |
| 101 | if (cfg.discordEnabled |
| 102 | if (cfg.telegramEnabled |
| 103 | await postTelegram(cfg |
| 104 | } |
| 105 | } catch (e) |
| 106 | console.error("[notifications] admin send failed: |
| 107 | } |
| 108 | })(); |
| 109 | } |
| 110 | |
| 111 | export function notifyServerOwner(ownerId |
| 112 | if (!ownerId) |
| 113 | void (async () |
| 114 | try { |
| 115 | const cfg = |
| 116 | if (!cfg) |
| 117 | const message = |
| 118 | const components = |
| 119 | if (cfg.discordWebhookUrl |
| 120 | if (cfg.telegramToken |
| 121 | await postTelegram(cfg |
| 122 | } |
| 123 | } catch (e) |
| 124 | console.error("[notifications] user send failed: |
| 125 | } |
| 126 | })(); |
| 127 | } |
| 128 | |
| 129 | export function buildMessage(kind |
| 130 | const prefix = |
| 131 | switch (kind) { |
| 132 | case "server-power": |
| 133 | return `[${prefix |
| 134 | case "backup": |
| 135 | return `[${prefix |
| 136 | case "server-created": |
| 137 | return `[${prefix |
| 138 | case "server-deleted": |
| 139 | return `[${prefix |
| 140 | case "user-deleted": |
| 141 | return `[${prefix |
| 142 | case "payment": |
| 143 | return `[${prefix |
| 144 | case "test": |
| 145 | return `[${prefix |
| 146 | default: |
| 147 | return `[${prefix |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | const DISCORD_COMPONENTS_V2_FLAG = 32768 |
| 152 | |
| 153 | export function buildDiscordComponents(kind |
| 154 | const appUrl = |
| 155 | let title = |
| 156 | let subtitle = |
| 157 | let detail = |
| 158 | let buttonLabel = |
| 159 | let buttonUrl: |
| 160 | |
| 161 | switch (kind) { |
| 162 | case "server-power": |
| 163 | title = "Power action" |
| 164 | subtitle = `${fields |
| 165 | detail = `**${ |
| 166 | buttonLabel = "View server" |
| 167 | if (appUrl && |
| 168 | break; |
| 169 | case "backup": { |
| 170 | const result = |
| 171 | title = "Backup update" |
| 172 | subtitle = `${fields |
| 173 | detail = `**${ |
| 174 | buttonLabel = "View server" |
| 175 | if (appUrl && |
| 176 | break; |
| 177 | } |
| 178 | case "server-created": |
| 179 | title = "Server created" |
| 180 | subtitle = `${fields |
| 181 | detail = `**${ |
| 182 | buttonLabel = "View server" |
| 183 | if (appUrl && |
| 184 | break; |
| 185 | case "server-deleted": |
| 186 | title = "Server deleted" |
| 187 | subtitle = `${fields |
| 188 | detail = `**${ |
| 189 | buttonLabel = "View servers" |
| 190 | if (appUrl) buttonUrl |
| 191 | break; |
| 192 | case "user-deleted": |
| 193 | title = "User deleted" |
| 194 | subtitle = "A user account has been deleted." |
| 195 | detail = `**${ |
| 196 | buttonLabel = "View users" |
| 197 | if (appUrl) buttonUrl |
| 198 | break; |
| 199 | case "payment": { |
| 200 | const received = |
| 201 | title = received |
| 202 | subtitle = received |
| 203 | detail = fields. |
| 204 | buttonLabel = "View billing" |
| 205 | if (appUrl) buttonUrl |
| 206 | break; |
| 207 | } |
| 208 | case "test": |
| 209 | title = "Test notification" |
| 210 | subtitle = "Notifications are working." |
| 211 | detail = `**${ |
| 212 | break; |
| 213 | } |
| 214 | |
| 215 | return componentsPayload({ |
| 216 | } |
| 217 | |
| 218 | function componentsPayload(v: |
| 219 | const header = |
| 220 | const inner: |
| 221 | if (v.buttonUrl |
| 222 | inner.push({ |
| 223 | type: 9, |
| 224 | components: [{ type: |
| 225 | accessory: { type: 2 |
| 226 | }); |
| 227 | inner.push({ |
| 228 | } else { |
| 229 | inner.push({ |
| 230 | } |
| 231 | inner.push({ |
| 232 | inner.push({ |
| 233 | return { |
| 234 | flags: DISCORD_COMPONENTS_V2_FLAG, |
| 235 | allowed_mentions: { parse: [] |
| 236 | components: [{ type: |
| 237 | }; |
| 238 | } |