seatalk-components 0.0.0-alpha1
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/README.md +25 -0
- package/index.js +1071 -0
- package/package.json +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# seatalk-components
|
|
2
|
+
|
|
3
|
+
seatalk-components is a versatile npm package designed to provide a wide range of reusable UI components for web development. It simplifies the process of building user interfaces by offering pre-built components that can be easily integrated into any project.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install seatalk-components, run the following command in your terminal:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install seatalk-components
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Responsive Design**: seatalk-components includes components that adapt to different screen sizes, ensuring a seamless user experience across devices.
|
|
16
|
+
- **Customization**: Each component offers customization options, allowing developers to tailor the appearance and behavior to their specific needs.
|
|
17
|
+
- **Accessibility**: seatalk-components prioritizes accessibility, ensuring that all components are compatible with assistive technologies and follow best practices for inclusivity.
|
|
18
|
+
|
|
19
|
+
## Contributing
|
|
20
|
+
|
|
21
|
+
Contributions to seatalk-components are welcome! If you have any ideas for new components or improvements to existing ones, please feel free to submit a pull request. We appreciate your help in making seatalk-components even better.
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
seatalk-components is released under the MIT license. See the LICENSE file for more information.
|
package/index.js
ADDED
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
// index.js
|
|
2
|
+
// This package provides data structures and utilities for handling SeaTalk-related components
|
|
3
|
+
// such as messages, users, teams, and channels, along with validation and formatting helpers.
|
|
4
|
+
// It focuses on structuring and validating data typically exchanged or used within a SeaTalk
|
|
5
|
+
// integration context, simulating component-like behavior through data manipulation and validation
|
|
6
|
+
// without relying on external UI frameworks.
|
|
7
|
+
|
|
8
|
+
// --- Constants ---
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {'text' | 'card' | 'file' | 'unknown'} SeaTalkMessageType
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enum for standard SeaTalk message types.
|
|
16
|
+
* @readonly
|
|
17
|
+
* @enum {SeaTalkMessageType}
|
|
18
|
+
*/
|
|
19
|
+
const SeaTalkMessageTypes = {
|
|
20
|
+
TEXT: 'text',
|
|
21
|
+
CARD: 'card',
|
|
22
|
+
FILE: 'file',
|
|
23
|
+
UNKNOWN: 'unknown'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {'user' | 'team' | 'channel' | 'unknown'} SeaTalkEntityType
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Enum for standard SeaTalk entity types.
|
|
32
|
+
* @readonly
|
|
33
|
+
* @enum {SeaTalkEntityType}
|
|
34
|
+
*/
|
|
35
|
+
const SeaTalkEntityTypes = {
|
|
36
|
+
USER: 'user',
|
|
37
|
+
TEAM: 'team',
|
|
38
|
+
CHANNEL: 'channel',
|
|
39
|
+
UNKNOWN: 'unknown'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maximum length for a text message content.
|
|
44
|
+
* @type {number}
|
|
45
|
+
*/
|
|
46
|
+
const MAX_TEXT_MESSAGE_LENGTH = 20000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Maximum length for a card title.
|
|
50
|
+
* @type {number}
|
|
51
|
+
*/
|
|
52
|
+
const MAX_CARD_TITLE_LENGTH = 1000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Maximum number of sections allowed in a card.
|
|
56
|
+
* @type {number}
|
|
57
|
+
*/
|
|
58
|
+
const MAX_CARD_SECTIONS = 20;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Maximum number of actions allowed in a card.
|
|
62
|
+
* @type {number}
|
|
63
|
+
*/
|
|
64
|
+
const MAX_CARD_ACTIONS = 10;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Regular expression for validating a basic URL format.
|
|
68
|
+
* @type {RegExp}
|
|
69
|
+
*/
|
|
70
|
+
const URL_REGEX = /^(?:\w+:)?\/\/[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Regular expression for validating a SeaTalk entity ID (basic alphanumeric, potentially with hyphens/underscores).
|
|
74
|
+
* @type {RegExp}
|
|
75
|
+
*/
|
|
76
|
+
const SEATALK_ID_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
77
|
+
|
|
78
|
+
// --- Utility Functions ---
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks if a value is a string.
|
|
82
|
+
* @param {*} value - The value to check.
|
|
83
|
+
* @returns {boolean} True if the value is a string, false otherwise.
|
|
84
|
+
*/
|
|
85
|
+
function isString(value) {
|
|
86
|
+
return typeof value === 'string';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks if a value is a number.
|
|
91
|
+
* @param {*} value - The value to check.
|
|
92
|
+
* @returns {boolean} True if the value is a number, false otherwise.
|
|
93
|
+
*/
|
|
94
|
+
function isNumber(value) {
|
|
95
|
+
return typeof value === 'number';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if a value is a boolean.
|
|
100
|
+
* @param {*} value - The value to check.
|
|
101
|
+
* @returns {boolean} True if the value is a boolean, false otherwise.
|
|
102
|
+
*/
|
|
103
|
+
function isBoolean(value) {
|
|
104
|
+
return typeof value === 'boolean';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Checks if a value is an array.
|
|
109
|
+
* @param {*} value - The value to check.
|
|
110
|
+
* @returns {boolean} True if the value is an array, false otherwise.
|
|
111
|
+
*/
|
|
112
|
+
function isArray(value) {
|
|
113
|
+
return Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if a value is an object (and not null or an array).
|
|
118
|
+
* @param {*} value - The value to check.
|
|
119
|
+
* @returns {boolean} True if the value is an object, false otherwise.
|
|
120
|
+
*/
|
|
121
|
+
function isObject(value) {
|
|
122
|
+
return typeof value === 'object' && value !== null && !isArray(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Checks if a string is a valid basic URL.
|
|
127
|
+
* @param {*} value - The value to check.
|
|
128
|
+
* @returns {boolean} True if the value is a valid URL string, false otherwise.
|
|
129
|
+
*/
|
|
130
|
+
function isValidUrl(value) {
|
|
131
|
+
if (!isString(value)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
// Use URL constructor for a stricter check first
|
|
136
|
+
new URL(value);
|
|
137
|
+
// Then check against a basic regex for common patterns not covered by URL constructor alone (like relative paths, though SeaTalk URLs are likely absolute)
|
|
138
|
+
return URL_REGEX.test(value);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return false; // Invalid URL constructor input
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Checks if a string is a valid non-empty SeaTalk entity ID.
|
|
146
|
+
* @param {*} value - The value to check.
|
|
147
|
+
* @returns {boolean} True if the value is a valid SeaTalk ID string, false otherwise.
|
|
148
|
+
*/
|
|
149
|
+
function isValidSeaTalkId(value) {
|
|
150
|
+
return isString(value) && value.length > 0 && SEATALK_ID_REGEX.test(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Formats a timestamp (Date object or number) into a SeaTalk-friendly string.
|
|
155
|
+
* Example: "YYYY-MM-DD HH:mm:ss"
|
|
156
|
+
* @param {Date | number} timestamp - The timestamp to format.
|
|
157
|
+
* @returns {string | null} The formatted date string or null if invalid input.
|
|
158
|
+
*/
|
|
159
|
+
function formatSeaTalkTimestamp(timestamp) {
|
|
160
|
+
let date;
|
|
161
|
+
if (timestamp instanceof Date) {
|
|
162
|
+
date = timestamp;
|
|
163
|
+
} else if (isNumber(timestamp)) {
|
|
164
|
+
date = new Date(timestamp);
|
|
165
|
+
} else {
|
|
166
|
+
// Try parsing as string if it's a string
|
|
167
|
+
if (isString(timestamp)) {
|
|
168
|
+
const parsedDate = new Date(timestamp);
|
|
169
|
+
if (!isNaN(parsedDate.getTime())) {
|
|
170
|
+
date = parsedDate;
|
|
171
|
+
} else {
|
|
172
|
+
return null; // Invalid string format
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
return null; // Invalid input type
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isNaN(date.getTime())) {
|
|
180
|
+
return null; // Invalid date value
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const pad = (num) => num.toString().padStart(2, '0');
|
|
184
|
+
|
|
185
|
+
const year = date.getFullYear();
|
|
186
|
+
const month = pad(date.getMonth() + 1); // Months are 0-indexed
|
|
187
|
+
const day = pad(date.getDate());
|
|
188
|
+
const hours = pad(date.getHours());
|
|
189
|
+
const minutes = pad(date.getMinutes());
|
|
190
|
+
const seconds = pad(date.getSeconds());
|
|
191
|
+
|
|
192
|
+
// Basic ISO-like format
|
|
193
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Escapes a string for potential use in a SeaTalk text message or card text (basic markdown escaping).
|
|
198
|
+
* @param {string} text - The text to escape.
|
|
199
|
+
* @returns {string} The escaped text.
|
|
200
|
+
*/
|
|
201
|
+
function escapeSeaTalkText(text) {
|
|
202
|
+
if (!isString(text)) {
|
|
203
|
+
return String(text); // Coerce to string if not already
|
|
204
|
+
}
|
|
205
|
+
// Escape common markdown special characters
|
|
206
|
+
return text.replace(/[\\`*_[\]()#+\-.!]/g, '\\$&');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Base Component / Entity Class ---
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Base class for all SeaTalk entities (User, Team, Channel, etc.).
|
|
213
|
+
* Provides common properties like ID and basic validation.
|
|
214
|
+
*/
|
|
215
|
+
class SeaTalkEntity {
|
|
216
|
+
/**
|
|
217
|
+
* @param {object} data - The raw data object for the entity.
|
|
218
|
+
* @param {string} data.id - The unique identifier for the entity.
|
|
219
|
+
* @param {SeaTalkEntityType} [entityType=SeaTalkEntityTypes.UNKNOWN] - The type of the entity.
|
|
220
|
+
* @throws {Error} If the ID is missing or invalid.
|
|
221
|
+
*/
|
|
222
|
+
constructor(data, entityType = SeaTalkEntityTypes.UNKNOWN) {
|
|
223
|
+
if (!isObject(data)) {
|
|
224
|
+
throw new Error(`Invalid initialization data for SeaTalkEntity. Expected object, got ${typeof data}.`);
|
|
225
|
+
}
|
|
226
|
+
if (!isValidSeaTalkId(data.id)) {
|
|
227
|
+
throw new Error(`Invalid or missing ID for SeaTalkEntity. ID: "${data.id}".`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The unique identifier for the entity.
|
|
232
|
+
* @type {string}
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
this._id = data.id;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The type of the entity.
|
|
239
|
+
* @type {SeaTalkEntityType}
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
this._type = entityType;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Gets the unique identifier of the entity.
|
|
247
|
+
* @returns {string} The entity ID.
|
|
248
|
+
*/
|
|
249
|
+
getId() {
|
|
250
|
+
return this._id;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Gets the type of the entity.
|
|
255
|
+
* @returns {SeaTalkEntityType} The entity type.
|
|
256
|
+
*/
|
|
257
|
+
getType() {
|
|
258
|
+
return this._type;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Validates the entity instance.
|
|
263
|
+
* @returns {boolean} True if the entity is valid.
|
|
264
|
+
* @throws {Error} If the entity is invalid.
|
|
265
|
+
*/
|
|
266
|
+
validate() {
|
|
267
|
+
if (!isValidSeaTalkId(this._id)) {
|
|
268
|
+
throw new Error(`Validation Error: Invalid ID "${this._id}" for ${this._type} entity.`);
|
|
269
|
+
}
|
|
270
|
+
if (!Object.values(SeaTalkEntityTypes).includes(this._type)) {
|
|
271
|
+
throw new Error(`Validation Error: Invalid internal type "${this._type}" for entity ID "${this._id}".`);
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Converts the entity instance to a plain JavaScript object.
|
|
278
|
+
* Should be overridden by subclasses.
|
|
279
|
+
* @returns {object} A plain object representation of the entity.
|
|
280
|
+
*/
|
|
281
|
+
toJSON() {
|
|
282
|
+
return {
|
|
283
|
+
id: this._id,
|
|
284
|
+
type: this._type,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// --- Specific Entity Components ---
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Represents a SeaTalk User entity.
|
|
293
|
+
*/
|
|
294
|
+
class SeaTalkUser extends SeaTalkEntity {
|
|
295
|
+
/**
|
|
296
|
+
* @param {object} data - The raw data object for the user.
|
|
297
|
+
* @param {string} data.id - The unique user ID.
|
|
298
|
+
* @param {string} data.name - The user's name.
|
|
299
|
+
* @param {string} [data.displayName] - The user's display name (if different).
|
|
300
|
+
* @param {string} [data.avatarUrl] - URL to the user's avatar image.
|
|
301
|
+
* @param {boolean} [data.isActive=true] - Whether the user account is active.
|
|
302
|
+
* @throws {Error} If required properties are missing or invalid.
|
|
303
|
+
*/
|
|
304
|
+
constructor(data) {
|
|
305
|
+
super(data, SeaTalkEntityTypes.USER);
|
|
306
|
+
|
|
307
|
+
if (!isString(data.name) || data.name.length === 0) {
|
|
308
|
+
throw new Error(`Invalid or missing 'name' for SeaTalkUser ID "${this.getId()}".`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** @private */ this._name = data.name;
|
|
312
|
+
/** @private */ this._displayName = isString(data.displayName) && data.displayName.length > 0 ? data.displayName : data.name;
|
|
313
|
+
/** @private */ this._avatarUrl = isValidUrl(data.avatarUrl) ? data.avatarUrl : undefined;
|
|
314
|
+
/** @private */ this._isActive = isBoolean(data.isActive) ? data.isActive : true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** @returns {string} The user's name. */
|
|
318
|
+
getName() { return this._name; }
|
|
319
|
+
/** @returns {string} The user's display name. */
|
|
320
|
+
getDisplayName() { return this._displayName; }
|
|
321
|
+
/** @returns {string | undefined} The avatar URL or undefined. */
|
|
322
|
+
getAvatarUrl() { return this._avatarUrl; }
|
|
323
|
+
/** @returns {boolean} True if active. */
|
|
324
|
+
isActive() { return this._isActive; }
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Validates the SeaTalkUser instance.
|
|
328
|
+
* @returns {boolean} True if the user is valid.
|
|
329
|
+
* @throws {Error} If the user is invalid.
|
|
330
|
+
*/
|
|
331
|
+
validate() {
|
|
332
|
+
super.validate();
|
|
333
|
+
if (!isString(this._name) || this._name.length === 0) {
|
|
334
|
+
throw new Error(`Validation Error: Invalid name "${this._name}" for user "${this.getId()}".`);
|
|
335
|
+
}
|
|
336
|
+
if (!isString(this._displayName) || this._displayName.length === 0) {
|
|
337
|
+
throw new Error(`Validation Error: Invalid display name "${this._displayName}" for user "${this.getId()}".`);
|
|
338
|
+
}
|
|
339
|
+
if (this._avatarUrl !== undefined && !isValidUrl(this._avatarUrl)) {
|
|
340
|
+
throw new Error(`Validation Error: Invalid avatar URL "${this._avatarUrl}" for user "${this.getId()}".`);
|
|
341
|
+
}
|
|
342
|
+
if (!isBoolean(this._isActive)) {
|
|
343
|
+
throw new Error(`Validation Error: Invalid isActive status for user "${this.getId()}".`);
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** @returns {object} A plain object representation of the user. */
|
|
349
|
+
toJSON() {
|
|
350
|
+
return {
|
|
351
|
+
...super.toJSON(),
|
|
352
|
+
name: this._name,
|
|
353
|
+
displayName: this._displayName,
|
|
354
|
+
avatarUrl: this._avatarUrl,
|
|
355
|
+
isActive: this._isActive,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Represents a SeaTalk Team entity.
|
|
362
|
+
*/
|
|
363
|
+
class SeaTalkTeam extends SeaTalkEntity {
|
|
364
|
+
/**
|
|
365
|
+
* @param {object} data - The raw data object for the team.
|
|
366
|
+
* @param {string} data.id - The unique team ID.
|
|
367
|
+
* @param {string} data.name - The team's name.
|
|
368
|
+
* @param {string} [data.description] - A description for the team.
|
|
369
|
+
* @throws {Error} If required properties are missing or invalid.
|
|
370
|
+
*/
|
|
371
|
+
constructor(data) {
|
|
372
|
+
super(data, SeaTalkEntityTypes.TEAM);
|
|
373
|
+
if (!isString(data.name) || data.name.length === 0) {
|
|
374
|
+
throw new Error(`Invalid or missing 'name' for SeaTalkTeam ID "${this.getId()}".`);
|
|
375
|
+
}
|
|
376
|
+
/** @private */ this._name = data.name;
|
|
377
|
+
/** @private */ this._description = isString(data.description) ? data.description : undefined;
|
|
378
|
+
}
|
|
379
|
+
/** @returns {string} The team's name. */
|
|
380
|
+
getName() { return this._name; }
|
|
381
|
+
/** @returns {string | undefined} The team description. */
|
|
382
|
+
getDescription() { return this._description; }
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Validates the SeaTalkTeam instance.
|
|
386
|
+
* @returns {boolean} True if the team is valid.
|
|
387
|
+
* @throws {Error} If the team is invalid.
|
|
388
|
+
*/
|
|
389
|
+
validate() {
|
|
390
|
+
super.validate();
|
|
391
|
+
if (!isString(this._name) || this._name.length === 0) {
|
|
392
|
+
throw new Error(`Validation Error: Invalid name "${this._name}" for team "${this.getId()}".`);
|
|
393
|
+
}
|
|
394
|
+
if (this._description !== undefined && !isString(this._description)) {
|
|
395
|
+
throw new Error(`Validation Error: Invalid description type for team "${this.getId()}".`);
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
/** @returns {object} A plain object representation of the team. */
|
|
400
|
+
toJSON() {
|
|
401
|
+
return {
|
|
402
|
+
...super.toJSON(),
|
|
403
|
+
name: this._name,
|
|
404
|
+
description: this._description,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Represents a SeaTalk Channel entity.
|
|
411
|
+
*/
|
|
412
|
+
class SeaTalkChannel extends SeaTalkEntity {
|
|
413
|
+
/**
|
|
414
|
+
* @param {object} data - The raw data object for the channel.
|
|
415
|
+
* @param {string} data.id - The unique channel ID.
|
|
416
|
+
* @param {string} data.name - The channel's name.
|
|
417
|
+
* @param {string} [data.topic] - A topic for the channel.
|
|
418
|
+
* @throws {Error} If required properties are missing or invalid.
|
|
419
|
+
*/
|
|
420
|
+
constructor(data) {
|
|
421
|
+
super(data, SeaTalkEntityTypes.CHANNEL);
|
|
422
|
+
if (!isString(data.name) || data.name.length === 0) {
|
|
423
|
+
throw new Error(`Invalid or missing 'name' for SeaTalkChannel ID "${this.getId()}".`);
|
|
424
|
+
}
|
|
425
|
+
/** @private */ this._name = data.name;
|
|
426
|
+
/** @private */ this._topic = isString(data.topic) ? data.topic : undefined;
|
|
427
|
+
}
|
|
428
|
+
/** @returns {string} The channel's name. */
|
|
429
|
+
getName() { return this._name; }
|
|
430
|
+
/** @returns {string | undefined} The channel topic. */
|
|
431
|
+
getTopic() { return this._topic; }
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Validates the SeaTalkChannel instance.
|
|
435
|
+
* @returns {boolean} True if the channel is valid.
|
|
436
|
+
* @throws {Error} If the channel is invalid.
|
|
437
|
+
*/
|
|
438
|
+
validate() {
|
|
439
|
+
super.validate();
|
|
440
|
+
if (!isString(this._name) || this._name.length === 0) {
|
|
441
|
+
throw new Error(`Validation Error: Invalid name "${this._name}" for channel "${this.getId()}".`);
|
|
442
|
+
}
|
|
443
|
+
if (this._topic !== undefined && !isString(this._topic)) {
|
|
444
|
+
throw new Error(`Validation Error: Invalid topic type for channel "${this.getId()}".`);
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
/** @returns {object} A plain object representation of the channel. */
|
|
449
|
+
toJSON() {
|
|
450
|
+
return {
|
|
451
|
+
...super.toJSON(),
|
|
452
|
+
name: this._name,
|
|
453
|
+
topic: this._topic,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// --- Base Message Class ---
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Base class for all SeaTalk messages.
|
|
462
|
+
* Provides common properties like ID, sender, timestamp, and type.
|
|
463
|
+
*/
|
|
464
|
+
class SeaTalkMessage {
|
|
465
|
+
/**
|
|
466
|
+
* @param {object} data - The raw data object for the message.
|
|
467
|
+
* @param {string} data.id - The unique identifier for the message.
|
|
468
|
+
* @param {string | object} data.sender - The ID string or raw object for the sender.
|
|
469
|
+
* @param {Date | number | string} [data.timestamp] - The timestamp when the message was sent (defaults to now).
|
|
470
|
+
* @param {SeaTalkMessageType} messageType - The specific type of the message.
|
|
471
|
+
* @throws {Error} If required properties are missing or invalid.
|
|
472
|
+
*/
|
|
473
|
+
constructor(data, messageType) {
|
|
474
|
+
if (!isObject(data)) {
|
|
475
|
+
throw new Error(`Invalid initialization data for SeaTalkMessage. Expected object, got ${typeof data}.`);
|
|
476
|
+
}
|
|
477
|
+
if (!isValidSeaTalkId(data.id)) {
|
|
478
|
+
throw new Error(`Invalid or missing ID for SeaTalkMessage. ID: "${data.id}".`);
|
|
479
|
+
}
|
|
480
|
+
if (!isString(data.sender) && !isObject(data.sender)) {
|
|
481
|
+
throw new Error(`Invalid or missing sender for SeaTalkMessage ID "${data.id}". Sender: "${data.sender}".`);
|
|
482
|
+
}
|
|
483
|
+
if (!Object.values(SeaTalkMessageTypes).includes(messageType)) {
|
|
484
|
+
throw new Error(`Invalid message type "${messageType}" provided for SeaTalkMessage ID "${data.id}".`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** @private */ this._id = data.id;
|
|
488
|
+
/** @private */ this._sender = data.sender;
|
|
489
|
+
/** @private */ this._timestamp = (data.timestamp instanceof Date || isNumber(data.timestamp))
|
|
490
|
+
? new Date(data.timestamp)
|
|
491
|
+
: (isString(data.timestamp) ? new Date(data.timestamp) : new Date());
|
|
492
|
+
|
|
493
|
+
if (isNaN(this._timestamp.getTime())) {
|
|
494
|
+
// If Date construction from string/number failed
|
|
495
|
+
throw new Error(`Invalid timestamp value "${data.timestamp}" for SeaTalkMessage ID "${data.id}".`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** @private */ this._type = messageType;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** @returns {string} The unique identifier of the message. */
|
|
502
|
+
getId() { return this._id; }
|
|
503
|
+
/** @returns {string | object} The sender's ID string or raw object. */
|
|
504
|
+
getSender() { return this._sender; }
|
|
505
|
+
/** @returns {Date} The timestamp of the message. */
|
|
506
|
+
getTimestamp() { return this._timestamp; }
|
|
507
|
+
/** @returns {string | null} The formatted timestamp string. */
|
|
508
|
+
getFormattedTimestamp() { return formatSeaTalkTimestamp(this._timestamp); }
|
|
509
|
+
/** @returns {SeaTalkMessageType} The type of the message. */
|
|
510
|
+
getType() { return this._type; }
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Checks if the message is of a specific type.
|
|
514
|
+
* @param {SeaTalkMessageType} type - The type to check against.
|
|
515
|
+
* @returns {boolean} True if the message type matches.
|
|
516
|
+
*/
|
|
517
|
+
isOfType(type) {
|
|
518
|
+
return this._type === type;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Validates the SeaTalkMessage instance.
|
|
523
|
+
* @returns {boolean} True if the message is valid.
|
|
524
|
+
* @throws {Error} If the message is invalid.
|
|
525
|
+
*/
|
|
526
|
+
validate() {
|
|
527
|
+
if (!isValidSeaTalkId(this._id)) {
|
|
528
|
+
throw new Error(`Validation Error: Invalid ID "${this._id}" for message of type "${this._type}".`);
|
|
529
|
+
}
|
|
530
|
+
if (!isString(this._sender) && !isObject(this._sender)) {
|
|
531
|
+
throw new Error(`Validation Error: Invalid sender "${this._sender}" for message ID "${this._id}".`);
|
|
532
|
+
}
|
|
533
|
+
if (isNaN(this._timestamp.getTime())) {
|
|
534
|
+
throw new Error(`Validation Error: Invalid timestamp for message ID "${this._id}".`);
|
|
535
|
+
}
|
|
536
|
+
if (!Object.values(SeaTalkMessageTypes).includes(this._type)) {
|
|
537
|
+
throw new Error(`Validation Error: Invalid internal type "${this._type}" for message ID "${this._id}".`);
|
|
538
|
+
}
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Converts the message instance to a plain JavaScript object.
|
|
544
|
+
* @returns {object} A plain object representation.
|
|
545
|
+
*/
|
|
546
|
+
toJSON() {
|
|
547
|
+
return {
|
|
548
|
+
id: this._id,
|
|
549
|
+
sender: this._sender, // Keep sender as is in base toJSON
|
|
550
|
+
timestamp: this._timestamp.toISOString(),
|
|
551
|
+
type: this._type,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// --- Specific Message Components ---
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Represents a SeaTalk Text Message.
|
|
560
|
+
*/
|
|
561
|
+
class SeaTalkTextMessage extends SeaTalkMessage {
|
|
562
|
+
/**
|
|
563
|
+
* @param {object} data - The raw data object for the text message.
|
|
564
|
+
* @param {string} data.id - The unique message ID.
|
|
565
|
+
* @param {string | object} data.sender - The ID string or raw object for the sender.
|
|
566
|
+
* @param {string} data.text - The text content.
|
|
567
|
+
* @param {Date | number | string} [data.timestamp] - The timestamp.
|
|
568
|
+
* @throws {Error} If required properties are missing or invalid.
|
|
569
|
+
*/
|
|
570
|
+
constructor(data) {
|
|
571
|
+
super(data, SeaTalkMessageTypes.TEXT);
|
|
572
|
+
|
|
573
|
+
if (!isString(data.text) || data.text.length === 0) {
|
|
574
|
+
throw new Error(`Invalid or missing 'text' content for SeaTalkTextMessage ID "${this.getId()}".`);
|
|
575
|
+
}
|
|
576
|
+
if (data.text.length > MAX_TEXT_MESSAGE_LENGTH) {
|
|
577
|
+
throw new Error(`'text' content exceeds maximum length of ${MAX_TEXT_MESSAGE_LENGTH} for SeaTalkTextMessage ID "${this.getId()}".`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** @private */ this._text = data.text;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** @returns {string} The text content. */
|
|
584
|
+
getText() { return this._text; }
|
|
585
|
+
/** @returns {string} The escaped text content. */
|
|
586
|
+
getEscapedText() { return escapeSeaTalkText(this._text); }
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Validates the SeaTalkTextMessage instance.
|
|
590
|
+
* @returns {boolean} True if valid.
|
|
591
|
+
* @throws {Error} If invalid.
|
|
592
|
+
*/
|
|
593
|
+
validate() {
|
|
594
|
+
super.validate();
|
|
595
|
+
if (!isString(this._text) || this._text.length === 0) {
|
|
596
|
+
throw new Error(`Validation Error: Invalid text content for text message "${this.getId()}".`);
|
|
597
|
+
}
|
|
598
|
+
if (this._text.length > MAX_TEXT_MESSAGE_LENGTH) {
|
|
599
|
+
throw new Error(`Validation Error: Text content exceeds max length for text message "${this.getId()}".`);
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
/** @returns {object} A plain object representation. */
|
|
604
|
+
toJSON() {
|
|
605
|
+
return {
|
|
606
|
+
...super.toJSON(),
|
|
607
|
+
text: this._text,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Represents a SeaTalk Card Message.
|
|
614
|
+
*/
|
|
615
|
+
class SeaTalkCardMessage extends SeaTalkMessage {
|
|
616
|
+
/**
|
|
617
|
+
* @param {object} data - The raw data object for the card message.
|
|
618
|
+
* @param {string} data.id - The unique message ID.
|
|
619
|
+
* @param {string | object} data.sender - The ID string or raw object for the sender.
|
|
620
|
+
* @param {object} data.card - The card structure object.
|
|
621
|
+
* @param {string} data.card.title - The title of the card.
|
|
622
|
+
* @param {Array<object>} [data.card.sections] - Array of card sections.
|
|
623
|
+
* @param {Array<object>} [data.card.actions] - Array of card actions.
|
|
624
|
+
* @param {Date | number | string} [data.timestamp] - The timestamp.
|
|
625
|
+
* @throws {Error} If required properties are missing or invalid.
|
|
626
|
+
*/
|
|
627
|
+
constructor(data) {
|
|
628
|
+
super(data, SeaTalkMessageTypes.CARD);
|
|
629
|
+
|
|
630
|
+
if (!isObject(data.card)) {
|
|
631
|
+
throw new Error(`Invalid or missing 'card' structure for SeaTalkCardMessage ID "${this.getId()}".`);
|
|
632
|
+
}
|
|
633
|
+
if (!isString(data.card.title) || data.card.title.length === 0) {
|
|
634
|
+
throw new Error(`Invalid or missing 'card.title' for SeaTalkCardMessage ID "${this.getId()}".`);
|
|
635
|
+
}
|
|
636
|
+
if (data.card.title.length > MAX_CARD_TITLE_LENGTH) {
|
|
637
|
+
throw new Error(`'card.title' exceeds maximum length of ${MAX_CARD_TITLE_LENGTH} for SeaTalkCardMessage ID "${this.getId()}".`);
|
|
638
|
+
}
|
|
639
|
+
if (data.card.sections !== undefined && (!isArray(data.card.sections) || data.card.sections.length > MAX_CARD_SECTIONS)) {
|
|
640
|
+
throw new Error(`Invalid or too many sections in 'card.sections' for SeaTalkCardMessage ID "${this.getId()}". Max ${MAX_CARD_SECTIONS}.`);
|
|
641
|
+
}
|
|
642
|
+
if (data.card.actions !== undefined && (!isArray(data.card.actions) || data.card.actions.length > MAX_CARD_ACTIONS)) {
|
|
643
|
+
throw new Error(`Invalid or too many actions in 'card.actions' for SeaTalkCardMessage ID "${this.getId()}". Max ${MAX_CARD_ACTIONS}.`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* @type {{title: string, sections: Array<object>, actions: Array<object>}}
|
|
648
|
+
* @private
|
|
649
|
+
*/
|
|
650
|
+
this._card = {
|
|
651
|
+
title: data.card.title,
|
|
652
|
+
sections: isArray(data.card.sections) ? data.card.sections.map(s => ({...s})) : [], // Clone sections
|
|
653
|
+
actions: isArray(data.card.actions) ? data.card.actions.map(a => ({...a})) : [], // Clone actions
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Perform initial basic validation of structure components
|
|
657
|
+
this._validateSections(this._card.sections, this.getId());
|
|
658
|
+
this._validateActions(this._card.actions, this.getId());
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** @returns {{title: string, sections: Array<object>, actions: Array<object>}} The card structure. */
|
|
662
|
+
getCard() { return {...this._card, sections: [...this._card.sections], actions: [...this._card.actions]}; } // Return clones
|
|
663
|
+
/** @returns {string} The card title. */
|
|
664
|
+
getCardTitle() { return this._card.title; }
|
|
665
|
+
/** @returns {Array<object>} An array of card sections (cloned). */
|
|
666
|
+
getCardSections() { return [...this._card.sections]; }
|
|
667
|
+
/** @returns {Array<object>} An array of card actions (cloned). */
|
|
668
|
+
getCardActions() { return [...this._card.actions]; }
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Adds a section to the card.
|
|
672
|
+
* @param {object} section - The section object to add.
|
|
673
|
+
* @throws {Error} If the section is invalid or exceeds max sections.
|
|
674
|
+
*/
|
|
675
|
+
addSection(section) {
|
|
676
|
+
if (!isObject(section)) {
|
|
677
|
+
throw new Error(`Invalid section object provided for card message "${this.getId()}".`);
|
|
678
|
+
}
|
|
679
|
+
if (this._card.sections.length >= MAX_CARD_SECTIONS) {
|
|
680
|
+
throw new Error(`Cannot add section. Maximum number of sections (${MAX_CARD_SECTIONS}) reached for card message "${this.getId()}".`);
|
|
681
|
+
}
|
|
682
|
+
// Perform basic validation on the section before adding
|
|
683
|
+
try {
|
|
684
|
+
this._validateSections([section], this.getId(), this._card.sections.length);
|
|
685
|
+
} catch (e) {
|
|
686
|
+
throw new Error(`Validation Error adding section to card message "${this.getId()}": ${e.message}`);
|
|
687
|
+
}
|
|
688
|
+
this._card.sections.push({...section}); // Add a clone
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Adds an action to the card.
|
|
693
|
+
* @param {object} action - The action object to add. Must have 'type' and 'text'.
|
|
694
|
+
* @throws {Error} If the action is invalid or exceeds max actions.
|
|
695
|
+
*/
|
|
696
|
+
addAction(action) {
|
|
697
|
+
if (!isObject(action) || !isString(action.type) || action.type.length === 0 || !isString(action.text) || action.text.length === 0) {
|
|
698
|
+
throw new Error(`Invalid action object provided for card message "${this.getId()}". Must have non-empty 'type' and 'text'.`);
|
|
699
|
+
}
|
|
700
|
+
if (this._card.actions.length >= MAX_CARD_ACTIONS) {
|
|
701
|
+
throw new Error(`Cannot add action. Maximum number of actions (${MAX_CARD_ACTIONS}) reached for card message "${this.getId()}".`);
|
|
702
|
+
}
|
|
703
|
+
// Perform basic validation on the action before adding
|
|
704
|
+
try {
|
|
705
|
+
this._validateActions([action], this.getId(), this._card.actions.length);
|
|
706
|
+
} catch (e) {
|
|
707
|
+
throw new Error(`Validation Error adding action to card message "${this.getId()}": ${e.message}`);
|
|
708
|
+
}
|
|
709
|
+
this._card.actions.push({...action}); // Add a clone
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Internal helper to validate an array of card sections.
|
|
714
|
+
* @private
|
|
715
|
+
* @param {Array<object>} sections - The sections array.
|
|
716
|
+
* @param {string} messageId - The ID of the parent message.
|
|
717
|
+
* @param {number} [startIndex=0] - The starting index for error reporting.
|
|
718
|
+
* @throws {Error} If validation fails.
|
|
719
|
+
*/
|
|
720
|
+
_validateSections(sections, messageId, startIndex = 0) {
|
|
721
|
+
if (!isArray(sections)) throw new Error('Internal Error: Sections must be an array.');
|
|
722
|
+
sections.forEach((section, index) => {
|
|
723
|
+
const currentIndex = startIndex + index;
|
|
724
|
+
if (!isObject(section)) {
|
|
725
|
+
throw new Error(`Invalid section structure at index ${currentIndex} for card message ID "${messageId}". Expected object.`);
|
|
726
|
+
}
|
|
727
|
+
// Example section validation: must have text or image or fields
|
|
728
|
+
const hasText = isString(section.text) && section.text.length > 0;
|
|
729
|
+
const hasImage = isObject(section.image);
|
|
730
|
+
const hasFields = isArray(section.fields) && section.fields.length > 0;
|
|
731
|
+
|
|
732
|
+
if (!hasText && !hasImage && !hasFields) {
|
|
733
|
+
throw new Error(`Section at index ${currentIndex} must contain 'text', 'image', or 'fields' for card message ID "${messageId}".`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// More detailed validation for section properties
|
|
737
|
+
if (section.text !== undefined && !isString(section.text)) {
|
|
738
|
+
throw new Error(`Invalid 'text' property type in section at index ${currentIndex} for card message ID "${messageId}". Expected string.`);
|
|
739
|
+
}
|
|
740
|
+
if (hasImage) {
|
|
741
|
+
if (!isString(section.image.url) || !isValidUrl(section.image.url)) {
|
|
742
|
+
throw new Error(`Invalid or missing 'url' property in 'image' object in section at index ${currentIndex} for card message ID "${messageId}". Expected valid URL string.`);
|
|
743
|
+
}
|
|
744
|
+
if (section.image.altText !== undefined && !isString(section.image.altText)) {
|
|
745
|
+
throw new Error(`Invalid 'altText' property type in 'image' object in section at index ${currentIndex} for card message ID "${messageId}". Expected string.`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (hasFields) {
|
|
749
|
+
section.fields.forEach((field, fieldIndex) => {
|
|
750
|
+
if (!isObject(field) || !isString(field.title) || field.title.length === 0 || !isString(field.value) || field.value.length === 0) {
|
|
751
|
+
throw new Error(`Invalid field structure at index ${fieldIndex} in section ${currentIndex} for card message ID "${messageId}". Must have non-empty 'title' and 'value'.`);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Internal helper to validate an array of card actions.
|
|
760
|
+
* @private
|
|
761
|
+
* @param {Array<object>} actions - The actions array.
|
|
762
|
+
* @param {string} messageId - The ID of the parent message.
|
|
763
|
+
* @param {number} [startIndex=0] - The starting index for error reporting.
|
|
764
|
+
* @throws {Error} If validation fails.
|
|
765
|
+
*/
|
|
766
|
+
_validateActions(actions, messageId, startIndex = 0) {
|
|
767
|
+
if (!isArray(actions)) throw new Error('Internal Error: Actions must be an array.');
|
|
768
|
+
actions.forEach((action, index) => {
|
|
769
|
+
const currentIndex = startIndex + index;
|
|
770
|
+
if (!isObject(action) || !isString(action.type) || action.type.length === 0 || !isString(action.text) || action.text.length === 0) {
|
|
771
|
+
throw new Error(`Invalid action structure at index ${currentIndex} for card message ID "${messageId}". Must have non-empty 'type' and 'text'.`);
|
|
772
|
+
}
|
|
773
|
+
// Detailed validation based on action type
|
|
774
|
+
if (action.type === 'open_url') {
|
|
775
|
+
if (!isString(action.url) || !isValidUrl(action.url)) {
|
|
776
|
+
throw new Error(`Invalid or missing 'url' property for 'open_url' action at index ${currentIndex} for card message ID "${messageId}". Expected valid URL string.`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Add validation for other action types here as needed (e.g., 'submit', 'message', etc.)
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Validates the SeaTalkCardMessage instance and its card structure.
|
|
786
|
+
* @returns {boolean} True if valid.
|
|
787
|
+
* @throws {Error} If invalid.
|
|
788
|
+
*/
|
|
789
|
+
validate() {
|
|
790
|
+
super.validate();
|
|
791
|
+
if (!isObject(this._card)) {
|
|
792
|
+
throw new Error(`Validation Error: Invalid internal card object for card message "${this.getId()}".`);
|
|
793
|
+
}
|
|
794
|
+
if (!isString(this._card.title) || this._card.title.length === 0) {
|
|
795
|
+
throw new Error(`Validation Error: Invalid card title "${this._card.title}" for card message "${this.getId()}".`);
|
|
796
|
+
}
|
|
797
|
+
if (this._card.title.length > MAX_CARD_TITLE_LENGTH) {
|
|
798
|
+
throw new Error(`Validation Error: Card title exceeds max length for card message "${this.getId()}".`);
|
|
799
|
+
}
|
|
800
|
+
if (!isArray(this._card.sections)) {
|
|
801
|
+
throw new Error(`Validation Error: Invalid internal sections array for card message "${this.getId()}".`);
|
|
802
|
+
}
|
|
803
|
+
if (this._card.sections.length > MAX_CARD_SECTIONS) {
|
|
804
|
+
throw new Error(`Validation Error: Too many sections for card message "${this.getId()}".`);
|
|
805
|
+
}
|
|
806
|
+
// Validate all current sections
|
|
807
|
+
this._validateSections(this._card.sections, this.getId());
|
|
808
|
+
|
|
809
|
+
if (!isArray(this._card.actions)) {
|
|
810
|
+
throw new Error(`Validation Error: Invalid internal actions array for card message "${this.getId()}".`);
|
|
811
|
+
}
|
|
812
|
+
if (this._card.actions.length > MAX_CARD_ACTIONS) {
|
|
813
|
+
throw new Error(`Validation Error: Too many actions for card message "${this.getId()}".`);
|
|
814
|
+
}
|
|
815
|
+
// Validate all current actions
|
|
816
|
+
this._validateActions(this._card.actions, this.getId());
|
|
817
|
+
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** @returns {object} A plain object representation. */
|
|
822
|
+
toJSON() {
|
|
823
|
+
return {
|
|
824
|
+
...super.toJSON(),
|
|
825
|
+
card: {
|
|
826
|
+
title: this._card.title,
|
|
827
|
+
sections: this._card.sections.map(s => ({...s})), // Return clones
|
|
828
|
+
actions: this._card.actions.map(a => ({...a})), // Return clones
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Represents a SeaTalk File Message.
|
|
836
|
+
*/
|
|
837
|
+
class SeaTalkFileMessage extends SeaTalkMessage {
|
|
838
|
+
/**
|
|
839
|
+
* @param {object} data - The raw data object for the file message.
|
|
840
|
+
* @param {string} data.id - The unique message ID.
|
|
841
|
+
* @param {string | object} data.sender - The ID string or raw object for the sender.
|
|
842
|
+
* @param {object} data.file - The file details object.
|
|
843
|
+
* @param {string} data.file.name - The name of the file.
|
|
844
|
+
* @param {number} data.file.size - The size of the file in bytes.
|
|
845
|
+
* @param {string} data.file.url - The URL to the file.
|
|
846
|
+
* @param {string} [data.file.mimeType] - The MIME type.
|
|
847
|
+
* @param {Date | number | string} [data.timestamp] - The timestamp.
|
|
848
|
+
* @throws {Error} If required properties are missing or invalid.
|
|
849
|
+
*/
|
|
850
|
+
constructor(data) {
|
|
851
|
+
super(data, SeaTalkMessageTypes.FILE);
|
|
852
|
+
|
|
853
|
+
if (!isObject(data.file)) {
|
|
854
|
+
throw new Error(`Invalid or missing 'file' structure for SeaTalkFileMessage ID "${this.getId()}".`);
|
|
855
|
+
}
|
|
856
|
+
if (!isString(data.file.name) || data.file.name.length === 0) {
|
|
857
|
+
throw new Error(`Invalid or missing 'file.name' for SeaTalkFileMessage ID "${this.getId()}".`);
|
|
858
|
+
}
|
|
859
|
+
if (!isNumber(data.file.size) || data.file.size < 0) {
|
|
860
|
+
throw new Error(`Invalid or missing 'file.size' for SeaTalkFileMessage ID "${this.getId()}". Must be a non-negative number.`);
|
|
861
|
+
}
|
|
862
|
+
if (!isString(data.file.url) || !isValidUrl(data.file.url)) {
|
|
863
|
+
throw new Error(`Invalid or missing 'file.url' for SeaTalkFileMessage ID "${this.getId()}".`);
|
|
864
|
+
}
|
|
865
|
+
if (data.file.mimeType !== undefined && !isString(data.file.mimeType)) {
|
|
866
|
+
throw new Error(`Invalid 'file.mimeType' type for SeaTalkFileMessage ID "${this.getId()}". Expected string.`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* @type {{name: string, size: number, url: string, mimeType?: string}}
|
|
871
|
+
* @private
|
|
872
|
+
*/
|
|
873
|
+
this._file = {
|
|
874
|
+
name: data.file.name,
|
|
875
|
+
size: data.file.size,
|
|
876
|
+
url: data.file.url,
|
|
877
|
+
mimeType: isString(data.file.mimeType) ? data.file.mimeType : undefined,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** @returns {{name: string, size: number, url: string, mimeType?: string}} The file details. */
|
|
882
|
+
getFile() { return {...this._file}; } // Return clone
|
|
883
|
+
/** @returns {string} The file name. */
|
|
884
|
+
getFileName() { return this._file.name; }
|
|
885
|
+
/** @returns {number} The file size. */
|
|
886
|
+
getFileSize() { return this._file.size; }
|
|
887
|
+
/** @returns {string} The file URL. */
|
|
888
|
+
getFileUrl() { return this._file.url; }
|
|
889
|
+
/** @returns {string | undefined} The file MIME type or undefined. */
|
|
890
|
+
getFileMimeType() { return this._file.mimeType; }
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Validates the SeaTalkFileMessage instance and its file structure.
|
|
894
|
+
* @returns {boolean} True if valid.
|
|
895
|
+
* @throws {Error} If invalid.
|
|
896
|
+
*/
|
|
897
|
+
validate() {
|
|
898
|
+
super.validate();
|
|
899
|
+
if (!isObject(this._file)) {
|
|
900
|
+
throw new Error(`Validation Error: Invalid internal file object for file message "${this.getId()}".`);
|
|
901
|
+
}
|
|
902
|
+
if (!isString(this._file.name) || this._file.name.length === 0) {
|
|
903
|
+
throw new Error(`Validation Error: Invalid file name "${this._file.name}" for file message "${this.getId()}".`);
|
|
904
|
+
}
|
|
905
|
+
if (!isNumber(this._file.size) || this._file.size < 0) {
|
|
906
|
+
throw new Error(`Validation Error: Invalid file size "${this._file.size}" for file message "${this.getId()}".`);
|
|
907
|
+
}
|
|
908
|
+
if (!isString(this._file.url) || !isValidUrl(this._file.url)) {
|
|
909
|
+
throw new Error(`Validation Error: Invalid file URL "${this._file.url}" for file message "${this.getId()}".`);
|
|
910
|
+
}
|
|
911
|
+
if (this._file.mimeType !== undefined && !isString(this._file.mimeType)) {
|
|
912
|
+
throw new Error(`Validation Error: Invalid file mimeType "${this._file.mimeType}" for file message "${this.getId()}".`);
|
|
913
|
+
}
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** @returns {object} A plain object representation. */
|
|
918
|
+
toJSON() {
|
|
919
|
+
return {
|
|
920
|
+
...super.toJSON(),
|
|
921
|
+
file: {...this._file}, // Return clone
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
// --- Factory / Parser Functions ---
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Attempts to parse a raw data object into a specific SeaTalk message class instance.
|
|
931
|
+
* It requires the 'type' property to determine which class to instantiate.
|
|
932
|
+
* @param {object} rawData - The raw data object received, potentially from an API.
|
|
933
|
+
* @returns {SeaTalkMessage | SeaTalkTextMessage | SeaTalkCardMessage | SeaTalkFileMessage | null} An instance of the appropriate message class, or null if parsing fails or type is unknown/invalid.
|
|
934
|
+
* @throws {Error} If basic message properties (id, sender, type) are missing or invalid.
|
|
935
|
+
*/
|
|
936
|
+
function parseSeaTalkMessage(rawData) {
|
|
937
|
+
if (!isObject(rawData)) {
|
|
938
|
+
console.error('parseSeaTalkMessage Error: Input is not an object.');
|
|
939
|
+
throw new Error('parseSeaTalkMessage Error: Input is not an object.');
|
|
940
|
+
}
|
|
941
|
+
if (!isValidSeaTalkId(rawData.id)) {
|
|
942
|
+
console.error(`parseSeaTalkMessage Error: Invalid or missing message ID "${rawData.id}".`);
|
|
943
|
+
throw new Error(`parseSeaTalkMessage Error: Invalid or missing message ID "${rawData.id}".`);
|
|
944
|
+
}
|
|
945
|
+
if (!isString(rawData.sender) && !isObject(rawData.sender)) {
|
|
946
|
+
console.error(`parseSeaTalkMessage Error: Invalid or missing message sender for ID "${rawData.id}".`);
|
|
947
|
+
throw new Error(`parseSeaTalkMessage Error: Invalid or missing message sender for ID "${rawData.id}".`);
|
|
948
|
+
}
|
|
949
|
+
if (!Object.values(SeaTalkMessageTypes).includes(rawData.type)) {
|
|
950
|
+
console.warn(`parseSeaTalkMessage Warning: Unknown or invalid message type "${rawData.type}" for message ID "${rawData.id}". Cannot parse into specific type.`);
|
|
951
|
+
return null; // Return null for unknown types
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
try {
|
|
955
|
+
switch (rawData.type) {
|
|
956
|
+
case SeaTalkMessageTypes.TEXT:
|
|
957
|
+
return new SeaTalkTextMessage(rawData);
|
|
958
|
+
case SeaTalkMessageTypes.CARD:
|
|
959
|
+
return new SeaTalkCardMessage(rawData);
|
|
960
|
+
case SeaTalkMessageTypes.FILE:
|
|
961
|
+
return new SeaTalkFileMessage(rawData);
|
|
962
|
+
case SeaTalkMessageTypes.UNKNOWN:
|
|
963
|
+
console.warn(`parseSeaTalkMessage Warning: Explicitly unknown message type for ID "${rawData.id}". Returning null.`);
|
|
964
|
+
return null;
|
|
965
|
+
default:
|
|
966
|
+
// This case should be covered by the initial type check, but as a safeguard
|
|
967
|
+
console.warn(`parseSeaTalkMessage Warning: Unhandled message type "${rawData.type}" for ID "${rawData.id}". Returning null.`);
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
} catch (error) {
|
|
971
|
+
// Catch errors during specific class construction (e.g., detailed validation errors)
|
|
972
|
+
console.error(`parseSeaTalkMessage Error: Failed to construct message instance for ID "${rawData.id}" with type "${rawData.type}".`, error.message);
|
|
973
|
+
throw new Error(`parseSeaTalkMessage Error: Failed to construct message instance for ID "${rawData.id}" with type "${rawData.type}". Details: ${error.message}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Attempts to parse a raw data object into a specific SeaTalk entity class instance.
|
|
979
|
+
* Requires the 'type' property to determine which class to instantiate.
|
|
980
|
+
* @param {object} rawData - The raw data object.
|
|
981
|
+
* @returns {SeaTalkEntity | SeaTalkUser | SeaTalkTeam | SeaTalkChannel | null} An instance of the appropriate entity class, or null if parsing fails or type is unknown/invalid.
|
|
982
|
+
* @throws {Error} If basic entity properties (id, type) are missing or invalid.
|
|
983
|
+
*/
|
|
984
|
+
function parseSeaTalkEntity(rawData) {
|
|
985
|
+
if (!isObject(rawData)) {
|
|
986
|
+
console.error('parseSeaTalkEntity Error: Input is not an object.');
|
|
987
|
+
throw new Error('parseSeaTalkEntity Error: Input is not an object.');
|
|
988
|
+
}
|
|
989
|
+
if (!isValidSeaTalkId(rawData.id)) {
|
|
990
|
+
console.error(`parseSeaTalkEntity Error: Invalid or missing entity ID "${rawData.id}".`);
|
|
991
|
+
throw new Error(`parseSeaTalkEntity Error: Invalid or missing entity ID "${rawData.id}".`);
|
|
992
|
+
}
|
|
993
|
+
if (!Object.values(SeaTalkEntityTypes).includes(rawData.type)) {
|
|
994
|
+
console.warn(`parseSeaTalkEntity Warning: Unknown or invalid entity type "${rawData.type}" for entity ID "${rawData.id}". Cannot parse into specific type.`);
|
|
995
|
+
return null; // Return null for unknown types
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
switch (rawData.type) {
|
|
1000
|
+
case SeaTalkEntityTypes.USER:
|
|
1001
|
+
return new SeaTalkUser(rawData);
|
|
1002
|
+
case SeaTalkEntityTypes.TEAM:
|
|
1003
|
+
return new SeaTalkTeam(rawData);
|
|
1004
|
+
case SeaTalkEntityTypes.CHANNEL:
|
|
1005
|
+
return new SeaTalkChannel(rawData);
|
|
1006
|
+
case SeaTalkEntityTypes.UNKNOWN:
|
|
1007
|
+
console.warn(`parseSeaTalkEntity Warning: Explicitly unknown entity type for ID "${rawData.id}". Returning base entity or null.`);
|
|
1008
|
+
try {
|
|
1009
|
+
return new SeaTalkEntity(rawData); // Return base if explicitly unknown type is given
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
console.error(`parseSeaTalkEntity Error: Failed to construct base entity for ID "${rawData.id}".`, e.message);
|
|
1012
|
+
return null; // Return null if even base construction fails
|
|
1013
|
+
}
|
|
1014
|
+
default:
|
|
1015
|
+
// This case should be covered by the initial type check, but as a safeguard
|
|
1016
|
+
console.warn(`parseSeaTalkEntity Warning: Unhandled entity type "${rawData.type}" for ID "${rawData.id}". Returning null.`);
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
// Catch errors during specific class construction (e.g., detailed validation errors)
|
|
1021
|
+
console.error(`parseSeaTalkEntity Error: Failed to construct entity instance for ID "${rawData.id}" with type "${rawData.type}".`, error.message);
|
|
1022
|
+
throw new Error(`parseSeaTalkEntity Error: Failed to construct entity instance for ID "${rawData.id}" with type "${rawData.type}". Details: ${error.message}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
// --- Main Export ---
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* @namespace SeaTalkComponents
|
|
1031
|
+
* @description Provides data structures, utilities, and validation for SeaTalk-related components.
|
|
1032
|
+
*/
|
|
1033
|
+
const SeaTalkComponents = {
|
|
1034
|
+
// Constants
|
|
1035
|
+
SeaTalkMessageTypes,
|
|
1036
|
+
SeaTalkEntityTypes,
|
|
1037
|
+
MAX_TEXT_MESSAGE_LENGTH,
|
|
1038
|
+
MAX_CARD_TITLE_LENGTH,
|
|
1039
|
+
MAX_CARD_SECTIONS,
|
|
1040
|
+
MAX_CARD_ACTIONS,
|
|
1041
|
+
|
|
1042
|
+
// Utility Functions
|
|
1043
|
+
isString,
|
|
1044
|
+
isNumber,
|
|
1045
|
+
isBoolean,
|
|
1046
|
+
isArray,
|
|
1047
|
+
isObject,
|
|
1048
|
+
isValidUrl,
|
|
1049
|
+
isValidSeaTalkId,
|
|
1050
|
+
formatSeaTalkTimestamp,
|
|
1051
|
+
escapeSeaTalkText,
|
|
1052
|
+
|
|
1053
|
+
// Entity Classes
|
|
1054
|
+
SeaTalkEntity,
|
|
1055
|
+
SeaTalkUser,
|
|
1056
|
+
SeaTalkTeam,
|
|
1057
|
+
SeaTalkChannel,
|
|
1058
|
+
|
|
1059
|
+
// Message Classes
|
|
1060
|
+
SeaTalkMessage,
|
|
1061
|
+
SeaTalkTextMessage,
|
|
1062
|
+
SeaTalkCardMessage,
|
|
1063
|
+
SeaTalkFileMessage,
|
|
1064
|
+
|
|
1065
|
+
// Parsing/Factory Functions
|
|
1066
|
+
parseSeaTalkMessage,
|
|
1067
|
+
parseSeaTalkEntity,
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// Export the main object
|
|
1071
|
+
module.exports = SeaTalkComponents;
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"author":"","description":"seatalk-components: A collection of reusable UI components for building modern web applications.","keywords":[],"license":"MIT","main":"index.js","name":"seatalk-components","scripts":{"test":"echo \"test\" \u0026\u0026 exit 1"},"version":"0.0.0-alpha1"}
|