@quilibrium/quorum-shared 2.1.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +2414 -0
- package/dist/index.d.ts +2414 -0
- package/dist/index.js +2788 -0
- package/dist/index.mjs +2678 -0
- package/package.json +49 -0
- package/src/api/client.ts +86 -0
- package/src/api/endpoints.ts +87 -0
- package/src/api/errors.ts +179 -0
- package/src/api/index.ts +35 -0
- package/src/crypto/encryption-state.ts +249 -0
- package/src/crypto/index.ts +55 -0
- package/src/crypto/types.ts +307 -0
- package/src/crypto/wasm-provider.ts +298 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/keys.ts +62 -0
- package/src/hooks/mutations/index.ts +15 -0
- package/src/hooks/mutations/useDeleteMessage.ts +67 -0
- package/src/hooks/mutations/useEditMessage.ts +87 -0
- package/src/hooks/mutations/useReaction.ts +163 -0
- package/src/hooks/mutations/useSendMessage.ts +131 -0
- package/src/hooks/useChannels.ts +49 -0
- package/src/hooks/useMessages.ts +77 -0
- package/src/hooks/useSpaces.ts +60 -0
- package/src/index.ts +32 -0
- package/src/signing/index.ts +10 -0
- package/src/signing/types.ts +83 -0
- package/src/signing/wasm-provider.ts +75 -0
- package/src/storage/adapter.ts +118 -0
- package/src/storage/index.ts +9 -0
- package/src/sync/index.ts +83 -0
- package/src/sync/service.test.ts +822 -0
- package/src/sync/service.ts +947 -0
- package/src/sync/types.ts +267 -0
- package/src/sync/utils.ts +588 -0
- package/src/transport/browser-websocket.ts +299 -0
- package/src/transport/index.ts +34 -0
- package/src/transport/rn-websocket.ts +321 -0
- package/src/transport/types.ts +56 -0
- package/src/transport/websocket.ts +212 -0
- package/src/types/bookmark.ts +29 -0
- package/src/types/conversation.ts +25 -0
- package/src/types/index.ts +57 -0
- package/src/types/message.ts +178 -0
- package/src/types/space.ts +75 -0
- package/src/types/user.ts +72 -0
- package/src/utils/encoding.ts +106 -0
- package/src/utils/formatting.ts +139 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/mentions.ts +135 -0
- package/src/utils/validation.ts +84 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encoding utilities for hex and base64 conversions
|
|
3
|
+
*
|
|
4
|
+
* These are platform-agnostic utilities for converting between
|
|
5
|
+
* byte arrays and string representations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert a hex string to a byte array
|
|
10
|
+
* @param hex - Hexadecimal string (with or without 0x prefix)
|
|
11
|
+
* @returns Array of bytes
|
|
12
|
+
*/
|
|
13
|
+
export function hexToBytes(hex: string): number[] {
|
|
14
|
+
// Remove 0x prefix if present
|
|
15
|
+
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
16
|
+
const bytes: number[] = [];
|
|
17
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
18
|
+
bytes.push(parseInt(cleanHex.substr(i, 2), 16));
|
|
19
|
+
}
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert a byte array to a hex string
|
|
25
|
+
* @param bytes - Array of bytes (number[] or Uint8Array)
|
|
26
|
+
* @returns Hexadecimal string (lowercase, no prefix)
|
|
27
|
+
*/
|
|
28
|
+
export function bytesToHex(bytes: number[] | Uint8Array): string {
|
|
29
|
+
return Array.from(bytes)
|
|
30
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
31
|
+
.join('');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert a base64 string to a Uint8Array
|
|
36
|
+
* @param base64 - Base64-encoded string
|
|
37
|
+
* @returns Uint8Array of bytes
|
|
38
|
+
*/
|
|
39
|
+
export function base64ToBytes(base64: string): Uint8Array {
|
|
40
|
+
// Handle both browser and Node.js environments
|
|
41
|
+
if (typeof atob === 'function') {
|
|
42
|
+
// Browser environment
|
|
43
|
+
const binaryString = atob(base64);
|
|
44
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
45
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
46
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
47
|
+
}
|
|
48
|
+
return bytes;
|
|
49
|
+
} else {
|
|
50
|
+
// Node.js environment
|
|
51
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert a byte array to a base64 string
|
|
57
|
+
* @param bytes - Array of bytes (number[] or Uint8Array)
|
|
58
|
+
* @returns Base64-encoded string
|
|
59
|
+
*/
|
|
60
|
+
export function bytesToBase64(bytes: number[] | Uint8Array): string {
|
|
61
|
+
// Handle both browser and Node.js environments
|
|
62
|
+
if (typeof btoa === 'function') {
|
|
63
|
+
// Browser environment
|
|
64
|
+
const binaryString = Array.from(bytes)
|
|
65
|
+
.map((b) => String.fromCharCode(b))
|
|
66
|
+
.join('');
|
|
67
|
+
return btoa(binaryString);
|
|
68
|
+
} else {
|
|
69
|
+
// Node.js environment
|
|
70
|
+
return Buffer.from(bytes).toString('base64');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert a UTF-8 string to a byte array
|
|
76
|
+
* @param str - UTF-8 string
|
|
77
|
+
* @returns Array of bytes
|
|
78
|
+
*/
|
|
79
|
+
export function stringToBytes(str: string): number[] {
|
|
80
|
+
const encoder = new TextEncoder();
|
|
81
|
+
return Array.from(encoder.encode(str));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convert a byte array to a UTF-8 string
|
|
86
|
+
* @param bytes - Array of bytes (number[] or Uint8Array)
|
|
87
|
+
* @returns UTF-8 string
|
|
88
|
+
*/
|
|
89
|
+
export function bytesToString(bytes: number[] | Uint8Array): string {
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
return decoder.decode(new Uint8Array(bytes));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Convert a number to an 8-byte big-endian array (int64)
|
|
96
|
+
* Used for timestamp serialization in signatures
|
|
97
|
+
*
|
|
98
|
+
* @param num - The number to convert (must be within safe integer range)
|
|
99
|
+
* @returns 8-byte Uint8Array in big-endian format
|
|
100
|
+
*/
|
|
101
|
+
export function int64ToBytes(num: number): Uint8Array {
|
|
102
|
+
const arr = new Uint8Array(8);
|
|
103
|
+
const view = new DataView(arr.buffer);
|
|
104
|
+
view.setBigInt64(0, BigInt(num), false);
|
|
105
|
+
return arr;
|
|
106
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date and text formatting utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format timestamp to time string (e.g., "2:30 PM")
|
|
7
|
+
*/
|
|
8
|
+
export function formatTime(timestamp: number): string {
|
|
9
|
+
const date = new Date(timestamp);
|
|
10
|
+
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format timestamp to date string (e.g., "Dec 24, 2025")
|
|
15
|
+
*/
|
|
16
|
+
export function formatDate(timestamp: number): string {
|
|
17
|
+
const date = new Date(timestamp);
|
|
18
|
+
return date.toLocaleDateString([], {
|
|
19
|
+
month: 'short',
|
|
20
|
+
day: 'numeric',
|
|
21
|
+
year: 'numeric',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format timestamp to full datetime string (e.g., "Dec 24, 2025 at 2:30 PM")
|
|
27
|
+
*/
|
|
28
|
+
export function formatDateTime(timestamp: number): string {
|
|
29
|
+
const date = new Date(timestamp);
|
|
30
|
+
return date.toLocaleString([], {
|
|
31
|
+
month: 'short',
|
|
32
|
+
day: 'numeric',
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
hour: 'numeric',
|
|
35
|
+
minute: '2-digit',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format timestamp to relative time (e.g., "5 minutes ago", "Yesterday")
|
|
41
|
+
*/
|
|
42
|
+
export function formatRelativeTime(timestamp: number): string {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const diff = now - timestamp;
|
|
45
|
+
|
|
46
|
+
const seconds = Math.floor(diff / 1000);
|
|
47
|
+
const minutes = Math.floor(seconds / 60);
|
|
48
|
+
const hours = Math.floor(minutes / 60);
|
|
49
|
+
const days = Math.floor(hours / 24);
|
|
50
|
+
|
|
51
|
+
if (seconds < 60) {
|
|
52
|
+
return 'Just now';
|
|
53
|
+
}
|
|
54
|
+
if (minutes < 60) {
|
|
55
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
|
|
56
|
+
}
|
|
57
|
+
if (hours < 24) {
|
|
58
|
+
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
59
|
+
}
|
|
60
|
+
if (days === 1) {
|
|
61
|
+
return 'Yesterday';
|
|
62
|
+
}
|
|
63
|
+
if (days < 7) {
|
|
64
|
+
return `${days} days ago`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return formatDate(timestamp);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format message date header (e.g., "Today", "Yesterday", "December 24, 2025")
|
|
72
|
+
*/
|
|
73
|
+
export function formatMessageDate(timestamp: number): string {
|
|
74
|
+
const date = new Date(timestamp);
|
|
75
|
+
const today = new Date();
|
|
76
|
+
const yesterday = new Date(today);
|
|
77
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
78
|
+
|
|
79
|
+
if (isSameDay(date, today)) {
|
|
80
|
+
return 'Today';
|
|
81
|
+
}
|
|
82
|
+
if (isSameDay(date, yesterday)) {
|
|
83
|
+
return 'Yesterday';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return date.toLocaleDateString([], {
|
|
87
|
+
weekday: 'long',
|
|
88
|
+
month: 'long',
|
|
89
|
+
day: 'numeric',
|
|
90
|
+
year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if two dates are the same day
|
|
96
|
+
*/
|
|
97
|
+
export function isSameDay(d1: Date, d2: Date): boolean {
|
|
98
|
+
return (
|
|
99
|
+
d1.getFullYear() === d2.getFullYear() &&
|
|
100
|
+
d1.getMonth() === d2.getMonth() &&
|
|
101
|
+
d1.getDate() === d2.getDate()
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Truncate text to a maximum length with ellipsis
|
|
107
|
+
*/
|
|
108
|
+
export function truncateText(text: string, maxLength: number): string {
|
|
109
|
+
if (text.length <= maxLength) {
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
return text.slice(0, maxLength - 1) + '\u2026';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Format file size (e.g., "1.5 MB")
|
|
117
|
+
*/
|
|
118
|
+
export function formatFileSize(bytes: number): string {
|
|
119
|
+
if (bytes === 0) return '0 B';
|
|
120
|
+
|
|
121
|
+
const k = 1024;
|
|
122
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
123
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
124
|
+
|
|
125
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Format member count (e.g., "1.2K members")
|
|
130
|
+
*/
|
|
131
|
+
export function formatMemberCount(count: number): string {
|
|
132
|
+
if (count < 1000) {
|
|
133
|
+
return `${count} member${count === 1 ? '' : 's'}`;
|
|
134
|
+
}
|
|
135
|
+
if (count < 1000000) {
|
|
136
|
+
return `${(count / 1000).toFixed(1)}K members`;
|
|
137
|
+
}
|
|
138
|
+
return `${(count / 1000000).toFixed(1)}M members`;
|
|
139
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility that respects build environment
|
|
3
|
+
*
|
|
4
|
+
* In development: logs to console
|
|
5
|
+
* In production: no-ops for performance
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { logger } from '@quorum/shared';
|
|
9
|
+
* logger.log('[MyModule]', 'some message', data);
|
|
10
|
+
* logger.warn('[MyModule]', 'warning message');
|
|
11
|
+
* logger.error('[MyModule]', 'error message', error);
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type LogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
|
|
15
|
+
|
|
16
|
+
interface LoggerConfig {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
minLevel: LogLevel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
22
|
+
debug: 0,
|
|
23
|
+
log: 1,
|
|
24
|
+
info: 2,
|
|
25
|
+
warn: 3,
|
|
26
|
+
error: 4,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Default config - can be overridden by calling logger.configure()
|
|
30
|
+
let config: LoggerConfig = {
|
|
31
|
+
enabled: true, // Will be set based on environment
|
|
32
|
+
minLevel: 'log',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Detect environment
|
|
36
|
+
function detectEnvironment(): boolean {
|
|
37
|
+
// React Native / Expo
|
|
38
|
+
if (typeof __DEV__ !== 'undefined') {
|
|
39
|
+
return __DEV__;
|
|
40
|
+
}
|
|
41
|
+
// Node.js / Electron
|
|
42
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
43
|
+
return process.env.NODE_ENV !== 'production';
|
|
44
|
+
}
|
|
45
|
+
// Browser
|
|
46
|
+
if (typeof window !== 'undefined') {
|
|
47
|
+
return window.location?.hostname === 'localhost';
|
|
48
|
+
}
|
|
49
|
+
// Default to enabled
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Initialize based on environment
|
|
54
|
+
config.enabled = detectEnvironment();
|
|
55
|
+
|
|
56
|
+
// No-op function for production
|
|
57
|
+
const noop = (): void => {};
|
|
58
|
+
|
|
59
|
+
function shouldLog(level: LogLevel): boolean {
|
|
60
|
+
if (!config.enabled) return false;
|
|
61
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[config.minLevel];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createLogMethod(level: LogLevel): (...args: unknown[]) => void {
|
|
65
|
+
return (...args: unknown[]) => {
|
|
66
|
+
if (shouldLog(level)) {
|
|
67
|
+
console[level](...args);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const logger = {
|
|
73
|
+
/**
|
|
74
|
+
* Configure the logger
|
|
75
|
+
*/
|
|
76
|
+
configure(newConfig: Partial<LoggerConfig>): void {
|
|
77
|
+
config = { ...config, ...newConfig };
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if logging is enabled
|
|
82
|
+
*/
|
|
83
|
+
isEnabled(): boolean {
|
|
84
|
+
return config.enabled;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Enable logging (useful for debugging production issues)
|
|
89
|
+
*/
|
|
90
|
+
enable(): void {
|
|
91
|
+
config.enabled = true;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Disable logging
|
|
96
|
+
*/
|
|
97
|
+
disable(): void {
|
|
98
|
+
config.enabled = false;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Log at debug level
|
|
103
|
+
*/
|
|
104
|
+
debug: createLogMethod('debug'),
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Log at default level
|
|
108
|
+
*/
|
|
109
|
+
log: createLogMethod('log'),
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Log at info level
|
|
113
|
+
*/
|
|
114
|
+
info: createLogMethod('info'),
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Log at warn level
|
|
118
|
+
*/
|
|
119
|
+
warn: createLogMethod('warn'),
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Log at error level (always logs unless explicitly disabled)
|
|
123
|
+
*/
|
|
124
|
+
error: createLogMethod('error'),
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a scoped logger with a prefix
|
|
128
|
+
*/
|
|
129
|
+
scope(prefix: string) {
|
|
130
|
+
return {
|
|
131
|
+
debug: (...args: unknown[]) => logger.debug(prefix, ...args),
|
|
132
|
+
log: (...args: unknown[]) => logger.log(prefix, ...args),
|
|
133
|
+
info: (...args: unknown[]) => logger.info(prefix, ...args),
|
|
134
|
+
warn: (...args: unknown[]) => logger.warn(prefix, ...args),
|
|
135
|
+
error: (...args: unknown[]) => logger.error(prefix, ...args),
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Type declaration for React Native's __DEV__ global
|
|
141
|
+
declare const __DEV__: boolean | undefined;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention parsing utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Mentions } from '../types';
|
|
6
|
+
|
|
7
|
+
/** Mention patterns */
|
|
8
|
+
export const MENTION_PATTERNS = {
|
|
9
|
+
user: /<@([a-zA-Z0-9]+)>/g,
|
|
10
|
+
role: /<@&([a-zA-Z0-9]+)>/g,
|
|
11
|
+
channel: /<#([a-zA-Z0-9]+)>/g,
|
|
12
|
+
everyone: /@everyone/g,
|
|
13
|
+
here: /@here/g,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Parsed mention */
|
|
17
|
+
export interface ParsedMention {
|
|
18
|
+
type: 'user' | 'role' | 'channel' | 'everyone' | 'here';
|
|
19
|
+
id?: string;
|
|
20
|
+
raw: string;
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse mentions from message text
|
|
27
|
+
*/
|
|
28
|
+
export function parseMentions(text: string): ParsedMention[] {
|
|
29
|
+
const mentions: ParsedMention[] = [];
|
|
30
|
+
|
|
31
|
+
// Parse user mentions
|
|
32
|
+
let match: RegExpExecArray | null;
|
|
33
|
+
const userRegex = new RegExp(MENTION_PATTERNS.user.source, 'g');
|
|
34
|
+
while ((match = userRegex.exec(text)) !== null) {
|
|
35
|
+
mentions.push({
|
|
36
|
+
type: 'user',
|
|
37
|
+
id: match[1],
|
|
38
|
+
raw: match[0],
|
|
39
|
+
start: match.index,
|
|
40
|
+
end: match.index + match[0].length,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parse role mentions
|
|
45
|
+
const roleRegex = new RegExp(MENTION_PATTERNS.role.source, 'g');
|
|
46
|
+
while ((match = roleRegex.exec(text)) !== null) {
|
|
47
|
+
mentions.push({
|
|
48
|
+
type: 'role',
|
|
49
|
+
id: match[1],
|
|
50
|
+
raw: match[0],
|
|
51
|
+
start: match.index,
|
|
52
|
+
end: match.index + match[0].length,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse channel mentions
|
|
57
|
+
const channelRegex = new RegExp(MENTION_PATTERNS.channel.source, 'g');
|
|
58
|
+
while ((match = channelRegex.exec(text)) !== null) {
|
|
59
|
+
mentions.push({
|
|
60
|
+
type: 'channel',
|
|
61
|
+
id: match[1],
|
|
62
|
+
raw: match[0],
|
|
63
|
+
start: match.index,
|
|
64
|
+
end: match.index + match[0].length,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Parse @everyone
|
|
69
|
+
const everyoneRegex = new RegExp(MENTION_PATTERNS.everyone.source, 'g');
|
|
70
|
+
while ((match = everyoneRegex.exec(text)) !== null) {
|
|
71
|
+
mentions.push({
|
|
72
|
+
type: 'everyone',
|
|
73
|
+
raw: match[0],
|
|
74
|
+
start: match.index,
|
|
75
|
+
end: match.index + match[0].length,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Parse @here
|
|
80
|
+
const hereRegex = new RegExp(MENTION_PATTERNS.here.source, 'g');
|
|
81
|
+
while ((match = hereRegex.exec(text)) !== null) {
|
|
82
|
+
mentions.push({
|
|
83
|
+
type: 'here',
|
|
84
|
+
raw: match[0],
|
|
85
|
+
start: match.index,
|
|
86
|
+
end: match.index + match[0].length,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sort by position
|
|
91
|
+
return mentions.sort((a, b) => a.start - b.start);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract Mentions object from parsed mentions
|
|
96
|
+
*/
|
|
97
|
+
export function extractMentions(text: string): Mentions {
|
|
98
|
+
const parsed = parseMentions(text);
|
|
99
|
+
|
|
100
|
+
const memberIds = parsed
|
|
101
|
+
.filter((m) => m.type === 'user' && m.id)
|
|
102
|
+
.map((m) => m.id!);
|
|
103
|
+
|
|
104
|
+
const roleIds = parsed
|
|
105
|
+
.filter((m) => m.type === 'role' && m.id)
|
|
106
|
+
.map((m) => m.id!);
|
|
107
|
+
|
|
108
|
+
const channelIds = parsed
|
|
109
|
+
.filter((m) => m.type === 'channel' && m.id)
|
|
110
|
+
.map((m) => m.id!);
|
|
111
|
+
|
|
112
|
+
const everyone = parsed.some((m) => m.type === 'everyone' || m.type === 'here');
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
memberIds: [...new Set(memberIds)],
|
|
116
|
+
roleIds: [...new Set(roleIds)],
|
|
117
|
+
channelIds: [...new Set(channelIds)],
|
|
118
|
+
everyone,
|
|
119
|
+
totalMentionCount: memberIds.length + roleIds.length + channelIds.length,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format mention for display
|
|
125
|
+
*/
|
|
126
|
+
export function formatMention(type: 'user' | 'role' | 'channel', id: string): string {
|
|
127
|
+
switch (type) {
|
|
128
|
+
case 'user':
|
|
129
|
+
return `<@${id}>`;
|
|
130
|
+
case 'role':
|
|
131
|
+
return `<@&${id}>`;
|
|
132
|
+
case 'channel':
|
|
133
|
+
return `<#${id}>`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Message, PostMessage } from '../types';
|
|
6
|
+
|
|
7
|
+
/** Maximum message length */
|
|
8
|
+
export const MAX_MESSAGE_LENGTH = 4000;
|
|
9
|
+
|
|
10
|
+
/** Maximum number of mentions per message */
|
|
11
|
+
export const MAX_MENTIONS = 50;
|
|
12
|
+
|
|
13
|
+
/** Validation result */
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
valid: boolean;
|
|
16
|
+
errors: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate message content before sending
|
|
21
|
+
*/
|
|
22
|
+
export function validateMessageContent(content: string): ValidationResult {
|
|
23
|
+
const errors: string[] = [];
|
|
24
|
+
|
|
25
|
+
if (!content || content.trim().length === 0) {
|
|
26
|
+
errors.push('Message cannot be empty');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (content.length > MAX_MESSAGE_LENGTH) {
|
|
30
|
+
errors.push(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
valid: errors.length === 0,
|
|
35
|
+
errors,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate a message object
|
|
41
|
+
*/
|
|
42
|
+
export function validateMessage(message: Partial<Message>): ValidationResult {
|
|
43
|
+
const errors: string[] = [];
|
|
44
|
+
|
|
45
|
+
if (!message.messageId) {
|
|
46
|
+
errors.push('Message ID is required');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!message.channelId) {
|
|
50
|
+
errors.push('Channel ID is required');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!message.spaceId) {
|
|
54
|
+
errors.push('Space ID is required');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!message.content) {
|
|
58
|
+
errors.push('Message content is required');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (message.content?.type === 'post') {
|
|
62
|
+
const postContent = message.content as PostMessage;
|
|
63
|
+
const text = Array.isArray(postContent.text)
|
|
64
|
+
? postContent.text.join('')
|
|
65
|
+
: postContent.text;
|
|
66
|
+
|
|
67
|
+
if (text.length > MAX_MESSAGE_LENGTH) {
|
|
68
|
+
errors.push(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
valid: errors.length === 0,
|
|
74
|
+
errors,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sanitize message content for display
|
|
80
|
+
*/
|
|
81
|
+
export function sanitizeContent(content: string): string {
|
|
82
|
+
// Remove null bytes and control characters (except newlines/tabs)
|
|
83
|
+
return content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
84
|
+
}
|