openvibers/openvibe.shared notifications.js
notifications.js
| 1 | ; |
| 2 | |
| 3 | // ═══════════════════════════════════════════════════════════════ |
| 4 | // OpenVibe — Universal Notification System |
| 5 | // Types, priorities, categories, and helpers shared across all |
| 6 | // services. Both server-side (Node) and client-side (browser). |
| 7 | // ═══════════════════════════════════════════════════════════════ |
| 8 | |
| 9 | // ── Priority Levels ────────────────────────────────────────── |
| 10 | // Determines toast style, sound, persistence, and email eligibility. |
| 11 | const PRIORITY = Object.freeze({ |
| 12 | LOW: 'low', // Silent, no toast, inbox only |
| 13 | NORMAL: 'normal', // Toast + badge, default sound |
| 14 | HIGH: 'high', // Sticky toast, alert sound, badge pulse |
| 15 | CRITICAL: 'critical', // Sticky + email-eligible, alarm sound, cannot dismiss silently |
| 16 | }); |
| 17 | |
| 18 | // ── Categories ─────────────────────────────────────────────── |
| 19 | // Users can configure per-category mute/sound/email preferences. |
| 20 | const CATEGORY = Object.freeze({ |
| 21 | SOCIAL: 'social', // Follows, friend requests, mentions, kisses |
| 22 | CHAT: 'chat', // DMs, @mentions, whispers |
| 23 | GAME: 'game', // Level-ups, loot, dungeon invites, canvas events |
| 24 | STREAM: 'stream', // Stream went live, raid, host, clip created |
| 25 | ECONOMY: 'economy', // Coins received, shop sales, trade offers |
| 26 | ACHIEVEMENT: 'achievement', // Badges, milestones, unlocks |
| 27 | MODERATION: 'moderation', // Warnings, bans, mutes, reports |
| 28 | SYSTEM: 'system', // Password changed, new login, security alerts |
| 29 | SERVICE: 'service', // Cross-service: linked accounts, SSO events |
| 30 | ADMIN: 'admin', // Admin broadcasts, maintenance notices |
| 31 | }); |
| 32 | |
| 33 | // ── Notification Types (expandable per-service) ────────────── |
| 34 | // Each type maps to a category + default priority + template. |
| 35 | const TYPES = Object.freeze({ |
| 36 | // Social |
| 37 | FOLLOW: { category: 'social', priority: 'normal', icon: '👤', title: 'New Follower' }, |
| 38 | MENTION: { category: 'social', priority: 'normal', icon: '@', title: 'Mentioned You' }, |
| 39 | FRIEND_REQUEST: { category: 'social', priority: 'normal', icon: '🤝', title: 'Friend Request' }, |
| 40 | |
| 41 | // Chat |
| 42 | DIRECT_MESSAGE: { category: 'chat', priority: 'normal', icon: '💬', title: 'New Message' }, |
| 43 | WHISPER: { category: 'chat', priority: 'normal', icon: '🔇', title: 'Whisper' }, |
| 44 | |
| 45 | // Game |
| 46 | LEVEL_UP: { category: 'game', priority: 'normal', icon: '⬆️', title: 'Level Up!' }, |
| 47 | LOOT_DROP: { category: 'game', priority: 'normal', icon: '🎁', title: 'Loot Drop' }, |
| 48 | DUNGEON_INVITE: { category: 'game', priority: 'high', icon: '🏰', title: 'Dungeon Invite' }, |
| 49 | BATTLE_CHALLENGE: { category: 'game', priority: 'high', icon: '⚔️', title: 'Battle Challenge' }, |
| 50 | CANVAS_OVERWRITE: { category: 'game', priority: 'low', icon: '🎨', title: 'Pixel Overwritten' }, |
| 51 | ACHIEVEMENT_UNLOCK: { category: 'achievement', priority: 'normal', icon: '🏆', title: 'Achievement Unlocked' }, |
| 52 | |
| 53 | // Stream |
| 54 | STREAM_LIVE: { category: 'stream', priority: 'high', icon: '🔴', title: 'Stream Live' }, |
| 55 | RAID: { category: 'stream', priority: 'high', icon: '🚀', title: 'Incoming Raid' }, |
| 56 | CLIP_CREATED: { category: 'stream', priority: 'normal', icon: '🎬', title: 'Clip Created' }, |
| 57 | VC_CALL_INVITE: { category: 'chat', priority: 'high', icon: '📞', title: 'Call Invite' }, |
| 58 | CONTENT_COMMENT: { category: 'social', priority: 'normal', icon: '💬', title: 'New Comment' }, |
| 59 | CONTENT_REPLY: { category: 'social', priority: 'normal', icon: '↩️', title: 'New Reply' }, |
| 60 | TIP_RECEIVED: { category: 'economy', priority: 'normal', icon: '💜', title: 'Tip Received' }, |
| 61 | SUBSCRIPTION: { category: 'economy', priority: 'normal', icon: '⭐', title: 'New Subscriber' }, |
| 62 | |
| 63 | // Economy |
| 64 | COINS_RECEIVED: { category: 'economy', priority: 'normal', icon: '🪙', title: 'Coins Received' }, |
| 65 | TRADE_OFFER: { category: 'economy', priority: 'normal', icon: '🤝', title: 'Trade Offer' }, |
| 66 | |
| 67 | // Moderation |
| 68 | WARNING: { category: 'moderation', priority: 'high', icon: '⚠️', title: 'Warning' }, |
| 69 | TIMEOUT: { category: 'moderation', priority: 'high', icon: '⏰', title: 'Timeout' }, |
| 70 | BAN: { category: 'moderation', priority: 'critical', icon: '🚫', title: 'Account Banned' }, |
| 71 | |
| 72 | // System / Security |
| 73 | PASSWORD_CHANGED: { category: 'system', priority: 'critical', icon: '🔒', title: 'Password Changed' }, |
| 74 | NEW_LOGIN: { category: 'system', priority: 'high', icon: '🔐', title: 'New Login Detected' }, |
| 75 | CONFIRMATION_REQUESTED: { category: 'system', priority: 'high', icon: '🔑', title: 'Approval needed' }, |
| 76 | EMAIL_VERIFIED: { category: 'system', priority: 'normal', icon: '✅', title: 'Email Verified' }, |
| 77 | ACCOUNT_LINKED: { category: 'system', priority: 'normal', icon: '🔗', title: 'Account Linked' }, |
| 78 | |
| 79 | // Service / Cross-platform |
| 80 | WELCOME: { category: 'service', priority: 'normal', icon: '🔥', title: 'Welcome to OpenVibe' }, |
| 81 | WATCH_TRIGGERED: { category: 'service', priority: 'normal', icon: '👁️', title: 'Watch Triggered' }, |
| 82 | SERVICE_ANNOUNCEMENT:{ category: 'admin', priority: 'high', icon: '📢', title: 'Announcement' }, |
| 83 | MAINTENANCE: { category: 'admin', priority: 'critical', icon: '🛠️', title: 'Scheduled Maintenance' }, |
| 84 | }); |
| 85 | |
| 86 | // ── Sound Map ──────────────────────────────────────────────── |
| 87 | // Priority → sound file (relative to /assets/sounds/ on each service). |
| 88 | const SOUNDS = Object.freeze({ |
| 89 | low: null, |
| 90 | normal: 'notification.mp3', |
| 91 | high: 'notification-high.mp3', |
| 92 | critical: 'notification-alarm.mp3', |
| 93 | }); |
| 94 | |
| 95 | // ── Email Eligibility ──────────────────────────────────────── |
| 96 | // Categories that are email-worthy by default when the user has not |
| 97 | // explicitly enabled email for a category-specific preference. |
| 98 | const EMAIL_ELIGIBLE_CATEGORIES = new Set([ |
| 99 | 'system', 'moderation', 'admin', |
| 100 | ]); |
| 101 | |
| 102 | // Types that email BY DEFAULT (no explicit per-category opt-in needed) — as long as the |
| 103 | // account's address is verified and not bouncing. Go-live alerts are the headline case: |
| 104 | // followers expect them in their inbox unless they turn the 'stream' category's email off. |
| 105 | const EMAIL_DEFAULT_TYPES = new Set(['STREAM_LIVE']); |
| 106 | |
| 107 | function isEmailEligible(notification) { |
| 108 | return notification.priority === PRIORITY.CRITICAL |
| 109 | && EMAIL_ELIGIBLE_CATEGORIES.has(notification.category); |
| 110 | } |
| 111 | |
| 112 | // ── Rich Content Schema ────────────────────────────────────── |
| 113 | // Notifications can carry optional rich payloads. |
| 114 | // |
| 115 | // richContent: { |
| 116 | // image?: string, // URL to preview image |
| 117 | // thumbnail?: string, // Small avatar/icon URL |
| 118 | // body?: string, // Markdown-safe extended body |
| 119 | // url?: string, // Click-through URL |
| 120 | // service?: string, // Origin service ID |
| 121 | // actions?: [ // Interactive buttons/inputs |
| 122 | // { id: string, label: string, style: 'primary'|'secondary'|'danger', url?: string }, |
| 123 | // { id: string, type: 'input', placeholder: string, submitLabel: string }, |
| 124 | // ], |
| 125 | // user?: { // Linked user display info |
| 126 | // id: number, username: string, display_name: string, |
| 127 | // avatar_url: string, profile_color: string, role: string, |
| 128 | // name_effect?: string, particle_effect?: string, |
| 129 | // }, |
| 130 | // context?: object, // Arbitrary service-specific data |
| 131 | // } |
| 132 | |
| 133 | // ── Notification Object Shape ──────────────────────────────── |
| 134 | // { |
| 135 | // id: string, // UUID |
| 136 | // type: string, // Key from TYPES |
| 137 | // category: string, // From CATEGORY |
| 138 | // priority: string, // From PRIORITY |
| 139 | // title: string, // Short headline |
| 140 | // message: string, // Plain-text body |
| 141 | // icon: string, // Emoji or icon class |
| 142 | // user_id: number, // Recipient |
| 143 | // sender_id?: number, // Who triggered it (nullable) |
| 144 | // service: string, // Origin service ('live', 'games', 'network') |
| 145 | // richContent?: object, // See schema above |
| 146 | // read: boolean, |
| 147 | // dismissed: boolean, |
| 148 | // emailed: boolean, |
| 149 | // created_at: string, // ISO 8601 |
| 150 | // expires_at?: string, // Optional auto-expire |
| 151 | // } |
| 152 | |
| 153 | function createNotification({ type, message, userId, senderId, service, richContent, expiresAt }) { |
| 154 | const typeDef = TYPES[type]; |
| 155 | if (!typeDef) throw new Error(`Unknown notification type: ${type}`); |
| 156 | |
| 157 | return { |
| 158 | type, |
| 159 | category: typeDef.category, |
| 160 | priority: typeDef.priority, |
| 161 | title: typeDef.title, |
| 162 | icon: typeDef.icon, |
| 163 | message: message || '', |
| 164 | user_id: userId, |
| 165 | sender_id: senderId || null, |
| 166 | service: service || 'network', |
| 167 | richContent: richContent || null, |
| 168 | read: false, |
| 169 | dismissed: false, |
| 170 | emailed: false, |
| 171 | expires_at: expiresAt || null, |
| 172 | }; |
| 173 | } |
| 174 | |
| 175 | // ── User Preferences Defaults ──────────────────────────────── |
| 176 | const DEFAULT_NOTIFICATION_PREFS = Object.freeze({ |
| 177 | enabled: true, |
| 178 | sound: true, |
| 179 | toasts: true, |
| 180 | email_critical: true, // Email for CRITICAL only |
| 181 | muted_categories: [], // Categories the user has silenced |
| 182 | do_not_disturb: false, |
| 183 | dnd_schedule: null, // { start: '22:00', end: '08:00', timezone: 'America/Los_Angeles' } |
| 184 | }); |
| 185 | |
| 186 | module.exports = { |
| 187 | EMAIL_DEFAULT_TYPES, |
| 188 | PRIORITY, |
| 189 | CATEGORY, |
| 190 | TYPES, |
| 191 | SOUNDS, |
| 192 | EMAIL_ELIGIBLE_CATEGORIES, |
| 193 | isEmailEligible, |
| 194 | createNotification, |
| 195 | DEFAULT_NOTIFICATION_PREFS, |
| 196 | }; |
| 197 |