openvibers/openvibe.shared notifications.js

notifications.js

main 10.7 KB · 197 lines · javascript

1'use strict';
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.
11const 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.
20const 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.
35const 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).
88const 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.
98const 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.
105const EMAIL_DEFAULT_TYPES = new Set(['STREAM_LIVE']);
106
107function 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
153function 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 ────────────────────────────────
176const 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
186module.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