waengine 1.7.3 → 1.7.4

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/src/scheduler.js CHANGED
@@ -10,12 +10,25 @@ export class Scheduler {
10
10
  // Gespeicherte Jobs beim Start laden
11
11
  this.loadScheduledJobs();
12
12
 
13
- console.log('📅 Scheduler initialisiert');
13
+ // Stille Initialisierung - keine Console-Spam
14
14
  }
15
15
 
16
16
  // ===== CRON SCHEDULING =====
17
17
 
18
18
  schedule(cronExpression, chatId, message, options = {}) {
19
+ // Input-Validierung
20
+ if (!cronExpression || typeof cronExpression !== 'string') {
21
+ throw new Error('❌ Cron-Ausdruck ist erforderlich');
22
+ }
23
+
24
+ if (!chatId || typeof chatId !== 'string') {
25
+ throw new Error('❌ Chat-ID ist erforderlich');
26
+ }
27
+
28
+ if (!message || typeof message !== 'string') {
29
+ throw new Error('❌ Nachricht ist erforderlich');
30
+ }
31
+
19
32
  const jobId = options.id || `job_${Date.now()}`;
20
33
 
21
34
  if (!cron.validate(cronExpression)) {
@@ -0,0 +1,678 @@
1
+ import { getStorage } from "./storage.js";
2
+ import crypto from "crypto";
3
+
4
+ export class SecurityManager {
5
+ constructor(client) {
6
+ this.client = client;
7
+ this.storage = getStorage();
8
+ this.rateLimits = new Map();
9
+ this.blockedUsers = new Set();
10
+ this.suspiciousActivity = new Map();
11
+ this.encryptionKeys = new Map();
12
+ this.auditLog = [];
13
+
14
+ this.initializeSecurity();
15
+ }
16
+
17
+ // ===== INITIALIZATION =====
18
+
19
+ initializeSecurity() {
20
+ this.loadSecurityConfig();
21
+ this.startSecurityMonitoring();
22
+ }
23
+
24
+ loadSecurityConfig() {
25
+ const config = this.storage.read.from("security").get("config") || {};
26
+
27
+ this.config = {
28
+ rateLimitEnabled: config.rateLimitEnabled !== false,
29
+ maxMessagesPerMinute: config.maxMessagesPerMinute || 10,
30
+ maxMessagesPerHour: config.maxMessagesPerHour || 100,
31
+ spamDetectionEnabled: config.spamDetectionEnabled !== false,
32
+ encryptionEnabled: config.encryptionEnabled || false,
33
+ auditLogEnabled: config.auditLogEnabled !== false,
34
+ suspiciousActivityThreshold: config.suspiciousActivityThreshold || 5,
35
+ autoBlockEnabled: config.autoBlockEnabled || false,
36
+ ...config
37
+ };
38
+
39
+ // Load blocked users
40
+ const blocked = this.storage.read.from("security").get("blockedUsers") || [];
41
+ blocked.forEach(userId => this.blockedUsers.add(userId));
42
+ }
43
+
44
+ startSecurityMonitoring() {
45
+ // Clean up old rate limit data every minute
46
+ setInterval(() => {
47
+ this.cleanupRateLimits();
48
+ }, 60000);
49
+
50
+ // Clean up suspicious activity data every hour
51
+ setInterval(() => {
52
+ this.cleanupSuspiciousActivity();
53
+ }, 3600000);
54
+ }
55
+
56
+ // ===== RATE LIMITING =====
57
+
58
+ /**
59
+ * Check if user is rate limited
60
+ */
61
+ checkRateLimit(userId) {
62
+ if (!this.config.rateLimitEnabled) return { allowed: true };
63
+
64
+ const now = Date.now();
65
+ const userLimits = this.rateLimits.get(userId) || { messages: [], lastReset: now };
66
+
67
+ // Clean old messages (older than 1 hour)
68
+ userLimits.messages = userLimits.messages.filter(timestamp =>
69
+ now - timestamp < 3600000
70
+ );
71
+
72
+ // Check per-minute limit
73
+ const recentMessages = userLimits.messages.filter(timestamp =>
74
+ now - timestamp < 60000
75
+ );
76
+
77
+ if (recentMessages.length >= this.config.maxMessagesPerMinute) {
78
+ this.logSecurityEvent('rate_limit_exceeded', userId, {
79
+ type: 'per_minute',
80
+ count: recentMessages.length,
81
+ limit: this.config.maxMessagesPerMinute
82
+ });
83
+
84
+ return {
85
+ allowed: false,
86
+ reason: 'rate_limit_per_minute',
87
+ resetIn: 60000 - (now - Math.min(...recentMessages))
88
+ };
89
+ }
90
+
91
+ // Check per-hour limit
92
+ if (userLimits.messages.length >= this.config.maxMessagesPerHour) {
93
+ this.logSecurityEvent('rate_limit_exceeded', userId, {
94
+ type: 'per_hour',
95
+ count: userLimits.messages.length,
96
+ limit: this.config.maxMessagesPerHour
97
+ });
98
+
99
+ return {
100
+ allowed: false,
101
+ reason: 'rate_limit_per_hour',
102
+ resetIn: 3600000 - (now - Math.min(...userLimits.messages))
103
+ };
104
+ }
105
+
106
+ // Add current message
107
+ userLimits.messages.push(now);
108
+ this.rateLimits.set(userId, userLimits);
109
+
110
+ return { allowed: true };
111
+ }
112
+
113
+ /**
114
+ * Set custom rate limit for user
115
+ */
116
+ setUserRateLimit(userId, messagesPerMinute, messagesPerHour) {
117
+ this.storage.write.in("security").set(`customRateLimits.${userId}`, {
118
+ messagesPerMinute,
119
+ messagesPerHour,
120
+ setAt: Date.now()
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Remove rate limit for user
126
+ */
127
+ removeUserRateLimit(userId) {
128
+ this.rateLimits.delete(userId);
129
+ this.storage.delete.from("security").key(`customRateLimits.${userId}`);
130
+ }
131
+
132
+ // ===== SPAM DETECTION =====
133
+
134
+ /**
135
+ * Analyze message for spam
136
+ */
137
+ analyzeSpam(message) {
138
+ if (!this.config.spamDetectionEnabled) return { isSpam: false };
139
+
140
+ const text = message.text || '';
141
+ let spamScore = 0;
142
+ const reasons = [];
143
+
144
+ // Check for excessive caps
145
+ const capsRatio = (text.match(/[A-Z]/g) || []).length / text.length;
146
+ if (capsRatio > 0.7 && text.length > 10) {
147
+ spamScore += 2;
148
+ reasons.push('excessive_caps');
149
+ }
150
+
151
+ // Check for excessive punctuation
152
+ const punctuationRatio = (text.match(/[!?.,;:]/g) || []).length / text.length;
153
+ if (punctuationRatio > 0.3) {
154
+ spamScore += 1;
155
+ reasons.push('excessive_punctuation');
156
+ }
157
+
158
+ // Check for repeated characters
159
+ if (/(.)\1{4,}/.test(text)) {
160
+ spamScore += 2;
161
+ reasons.push('repeated_characters');
162
+ }
163
+
164
+ // Check for common spam words
165
+ const spamWords = [
166
+ 'gewinn', 'gratis', 'kostenlos', 'sofort', 'jetzt', 'schnell',
167
+ 'geld', 'verdienen', 'reich', 'millionär', 'bitcoin', 'crypto',
168
+ 'klick', 'link', 'website', 'angebot', 'rabatt', 'prozent'
169
+ ];
170
+
171
+ const foundSpamWords = spamWords.filter(word =>
172
+ text.toLowerCase().includes(word)
173
+ );
174
+
175
+ if (foundSpamWords.length > 2) {
176
+ spamScore += foundSpamWords.length;
177
+ reasons.push('spam_keywords');
178
+ }
179
+
180
+ // Check for URLs
181
+ const urlCount = (text.match(/https?:\/\/[^\s]+/g) || []).length;
182
+ if (urlCount > 1) {
183
+ spamScore += urlCount;
184
+ reasons.push('multiple_urls');
185
+ }
186
+
187
+ // Check message frequency
188
+ const recentMessages = this.rateLimits.get(message.from)?.messages || [];
189
+ const recentCount = recentMessages.filter(timestamp =>
190
+ Date.now() - timestamp < 300000 // 5 minutes
191
+ ).length;
192
+
193
+ if (recentCount > 5) {
194
+ spamScore += Math.floor(recentCount / 5);
195
+ reasons.push('high_frequency');
196
+ }
197
+
198
+ const isSpam = spamScore >= 5;
199
+
200
+ if (isSpam) {
201
+ this.logSecurityEvent('spam_detected', message.from, {
202
+ score: spamScore,
203
+ reasons,
204
+ text: text.substring(0, 100)
205
+ });
206
+ }
207
+
208
+ return {
209
+ isSpam,
210
+ score: spamScore,
211
+ reasons,
212
+ confidence: Math.min(spamScore / 10, 1)
213
+ };
214
+ }
215
+
216
+ // ===== USER BLOCKING =====
217
+
218
+ /**
219
+ * Block user
220
+ */
221
+ blockUser(userId, reason = 'manual', duration = null) {
222
+ this.blockedUsers.add(userId);
223
+
224
+ const blockData = {
225
+ userId,
226
+ reason,
227
+ blockedAt: Date.now(),
228
+ duration,
229
+ expiresAt: duration ? Date.now() + duration : null
230
+ };
231
+
232
+ this.storage.write.in("security").set(`blocks.${userId}`, blockData);
233
+ this.storage.write.in("security").push("blockedUsers", userId);
234
+
235
+ this.logSecurityEvent('user_blocked', userId, { reason, duration });
236
+
237
+ return blockData;
238
+ }
239
+
240
+ /**
241
+ * Unblock user
242
+ */
243
+ unblockUser(userId) {
244
+ this.blockedUsers.delete(userId);
245
+ this.storage.delete.from("security").key(`blocks.${userId}`);
246
+
247
+ // Remove from blocked users list
248
+ const blockedList = this.storage.read.from("security").get("blockedUsers") || [];
249
+ const updatedList = blockedList.filter(id => id !== userId);
250
+ this.storage.write.in("security").set("blockedUsers", updatedList);
251
+
252
+ this.logSecurityEvent('user_unblocked', userId);
253
+
254
+ return true;
255
+ }
256
+
257
+ /**
258
+ * Check if user is blocked
259
+ */
260
+ isUserBlocked(userId) {
261
+ if (!this.blockedUsers.has(userId)) return false;
262
+
263
+ // Check if temporary block has expired
264
+ const blockData = this.storage.read.from("security").get(`blocks.${userId}`);
265
+ if (blockData && blockData.expiresAt && Date.now() > blockData.expiresAt) {
266
+ this.unblockUser(userId);
267
+ return false;
268
+ }
269
+
270
+ return true;
271
+ }
272
+
273
+ /**
274
+ * Auto-block user based on suspicious activity
275
+ */
276
+ checkAutoBlock(userId) {
277
+ if (!this.config.autoBlockEnabled) return false;
278
+
279
+ const activity = this.suspiciousActivity.get(userId) || { count: 0, events: [] };
280
+
281
+ if (activity.count >= this.config.suspiciousActivityThreshold) {
282
+ this.blockUser(userId, 'auto_block_suspicious_activity', 24 * 60 * 60 * 1000); // 24 hours
283
+ return true;
284
+ }
285
+
286
+ return false;
287
+ }
288
+
289
+ // ===== SUSPICIOUS ACTIVITY TRACKING =====
290
+
291
+ /**
292
+ * Track suspicious activity
293
+ */
294
+ trackSuspiciousActivity(userId, type, data = {}) {
295
+ const activity = this.suspiciousActivity.get(userId) || { count: 0, events: [] };
296
+
297
+ activity.count++;
298
+ activity.events.push({
299
+ type,
300
+ data,
301
+ timestamp: Date.now()
302
+ });
303
+
304
+ // Keep only last 50 events
305
+ if (activity.events.length > 50) {
306
+ activity.events.shift();
307
+ }
308
+
309
+ this.suspiciousActivity.set(userId, activity);
310
+
311
+ this.logSecurityEvent('suspicious_activity', userId, { type, data });
312
+
313
+ // Check for auto-block
314
+ this.checkAutoBlock(userId);
315
+
316
+ return activity;
317
+ }
318
+
319
+ /**
320
+ * Get user's suspicious activity
321
+ */
322
+ getUserSuspiciousActivity(userId) {
323
+ return this.suspiciousActivity.get(userId) || { count: 0, events: [] };
324
+ }
325
+
326
+ // ===== MESSAGE ENCRYPTION =====
327
+
328
+ /**
329
+ * Encrypt message with secure AES-256-GCM
330
+ */
331
+ encryptMessage(message, userId) {
332
+ if (!this.config.encryptionEnabled) return message;
333
+
334
+ try {
335
+ const key = this.getOrCreateEncryptionKey(userId);
336
+ const keyBuffer = Buffer.from(key, 'hex');
337
+
338
+ // Generate random IV (12 bytes for GCM)
339
+ const iv = crypto.randomBytes(12);
340
+
341
+ // Create cipher with GCM mode for authenticated encryption
342
+ const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
343
+
344
+ let encrypted = cipher.update(message, 'utf8', 'hex');
345
+ encrypted += cipher.final('hex');
346
+
347
+ // Get authentication tag
348
+ const authTag = cipher.getAuthTag();
349
+
350
+ // Combine IV + authTag + encrypted data
351
+ const result = iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
352
+
353
+ return result;
354
+ } catch (error) {
355
+ console.error('❌ Encryption error:', error.message);
356
+ return message; // Fallback to unencrypted
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Decrypt message with secure AES-256-GCM
362
+ */
363
+ decryptMessage(encryptedMessage, userId) {
364
+ if (!this.config.encryptionEnabled) return encryptedMessage;
365
+
366
+ try {
367
+ const key = this.getOrCreateEncryptionKey(userId);
368
+ const keyBuffer = Buffer.from(key, 'hex');
369
+
370
+ // Split IV:authTag:encrypted
371
+ const parts = encryptedMessage.split(':');
372
+ if (parts.length !== 3) {
373
+ throw new Error('Invalid encrypted message format');
374
+ }
375
+
376
+ const iv = Buffer.from(parts[0], 'hex');
377
+ const authTag = Buffer.from(parts[1], 'hex');
378
+ const encrypted = parts[2];
379
+
380
+ // Create decipher
381
+ const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
382
+ decipher.setAuthTag(authTag);
383
+
384
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
385
+ decrypted += decipher.final('utf8');
386
+
387
+ return decrypted;
388
+ } catch (error) {
389
+ console.error('❌ Decryption error:', error.message);
390
+ return encryptedMessage; // Fallback to encrypted
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Get or create encryption key for user
396
+ */
397
+ getOrCreateEncryptionKey(userId) {
398
+ if (this.encryptionKeys.has(userId)) {
399
+ return this.encryptionKeys.get(userId);
400
+ }
401
+
402
+ const key = crypto.randomBytes(32).toString('hex');
403
+ this.encryptionKeys.set(userId, key);
404
+
405
+ // Store encrypted key
406
+ const masterKey = this.getMasterKey();
407
+ const encryptedKey = this.encryptWithMasterKey(key, masterKey);
408
+ this.storage.write.in("security").set(`encryptionKeys.${userId}`, encryptedKey);
409
+
410
+ return key;
411
+ }
412
+
413
+ /**
414
+ * Get master encryption key
415
+ */
416
+ getMasterKey() {
417
+ let masterKey = this.storage.read.from("security").get("masterKey");
418
+
419
+ if (!masterKey) {
420
+ masterKey = crypto.randomBytes(32).toString('hex');
421
+ this.storage.write.in("security").set("masterKey", masterKey);
422
+ }
423
+
424
+ return masterKey;
425
+ }
426
+
427
+ /**
428
+ * Encrypt with master key using secure AES-256-GCM
429
+ */
430
+ encryptWithMasterKey(data, masterKey) {
431
+ try {
432
+ const keyBuffer = Buffer.from(masterKey, 'hex');
433
+ const iv = crypto.randomBytes(12); // 12 bytes for GCM
434
+
435
+ const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
436
+
437
+ let encrypted = cipher.update(data, 'utf8', 'hex');
438
+ encrypted += cipher.final('hex');
439
+
440
+ const authTag = cipher.getAuthTag();
441
+
442
+ // Return IV:authTag:encrypted
443
+ return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
444
+ } catch (error) {
445
+ console.error('❌ Master key encryption error:', error.message);
446
+ throw error;
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Decrypt with master key using secure AES-256-GCM
452
+ */
453
+ decryptWithMasterKey(encryptedData, masterKey) {
454
+ try {
455
+ const keyBuffer = Buffer.from(masterKey, 'hex');
456
+
457
+ const parts = encryptedData.split(':');
458
+ if (parts.length !== 3) {
459
+ throw new Error('Invalid encrypted data format');
460
+ }
461
+
462
+ const iv = Buffer.from(parts[0], 'hex');
463
+ const authTag = Buffer.from(parts[1], 'hex');
464
+ const encrypted = parts[2];
465
+
466
+ const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
467
+ decipher.setAuthTag(authTag);
468
+
469
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
470
+ decrypted += decipher.final('utf8');
471
+
472
+ return decrypted;
473
+ } catch (error) {
474
+ console.error('❌ Master key decryption error:', error.message);
475
+ throw error;
476
+ }
477
+ }
478
+
479
+ // ===== AUDIT LOGGING =====
480
+
481
+ /**
482
+ * Log security event
483
+ */
484
+ logSecurityEvent(type, userId, data = {}) {
485
+ if (!this.config.auditLogEnabled) return;
486
+
487
+ const event = {
488
+ id: crypto.randomUUID(),
489
+ type,
490
+ userId,
491
+ data,
492
+ timestamp: Date.now(),
493
+ ip: data.ip || 'unknown'
494
+ };
495
+
496
+ this.auditLog.push(event);
497
+
498
+ // Keep only last 1000 events in memory
499
+ if (this.auditLog.length > 1000) {
500
+ this.auditLog.shift();
501
+ }
502
+
503
+ // Store in persistent storage
504
+ this.storage.write.in("security").push("auditLog", event);
505
+
506
+ // Emit security event
507
+ this.client.emit('security_event', event);
508
+
509
+ return event;
510
+ }
511
+
512
+ /**
513
+ * Get audit log
514
+ */
515
+ getAuditLog(limit = 100, type = null) {
516
+ let logs = this.storage.read.from("security").get("auditLog") || [];
517
+
518
+ if (type) {
519
+ logs = logs.filter(log => log.type === type);
520
+ }
521
+
522
+ return logs.slice(-limit);
523
+ }
524
+
525
+ /**
526
+ * Get security events for user
527
+ */
528
+ getUserSecurityEvents(userId, limit = 50) {
529
+ const logs = this.storage.read.from("security").get("auditLog") || [];
530
+ return logs
531
+ .filter(log => log.userId === userId)
532
+ .slice(-limit);
533
+ }
534
+
535
+ // ===== SECURITY ANALYSIS =====
536
+
537
+ /**
538
+ * Analyze message security
539
+ */
540
+ analyzeMessageSecurity(message) {
541
+ const analysis = {
542
+ userId: message.from,
543
+ timestamp: Date.now(),
544
+ checks: {}
545
+ };
546
+
547
+ // Rate limit check
548
+ analysis.checks.rateLimit = this.checkRateLimit(message.from);
549
+
550
+ // Spam check
551
+ analysis.checks.spam = this.analyzeSpam(message);
552
+
553
+ // Block check
554
+ analysis.checks.blocked = this.isUserBlocked(message.from);
555
+
556
+ // Suspicious activity check
557
+ analysis.checks.suspiciousActivity = this.getUserSuspiciousActivity(message.from);
558
+
559
+ // Overall risk score
560
+ let riskScore = 0;
561
+
562
+ if (!analysis.checks.rateLimit.allowed) riskScore += 3;
563
+ if (analysis.checks.spam.isSpam) riskScore += analysis.checks.spam.score;
564
+ if (analysis.checks.blocked) riskScore += 10;
565
+ if (analysis.checks.suspiciousActivity.count > 0) riskScore += analysis.checks.suspiciousActivity.count;
566
+
567
+ analysis.riskScore = riskScore;
568
+ analysis.riskLevel = this.getRiskLevel(riskScore);
569
+ analysis.shouldBlock = riskScore >= 10;
570
+
571
+ return analysis;
572
+ }
573
+
574
+ /**
575
+ * Get risk level from score
576
+ */
577
+ getRiskLevel(score) {
578
+ if (score >= 10) return 'high';
579
+ if (score >= 5) return 'medium';
580
+ if (score >= 2) return 'low';
581
+ return 'minimal';
582
+ }
583
+
584
+ // ===== CLEANUP METHODS =====
585
+
586
+ cleanupRateLimits() {
587
+ const now = Date.now();
588
+
589
+ for (const [userId, limits] of this.rateLimits.entries()) {
590
+ limits.messages = limits.messages.filter(timestamp =>
591
+ now - timestamp < 3600000 // Keep last hour
592
+ );
593
+
594
+ if (limits.messages.length === 0) {
595
+ this.rateLimits.delete(userId);
596
+ }
597
+ }
598
+ }
599
+
600
+ cleanupSuspiciousActivity() {
601
+ const now = Date.now();
602
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
603
+
604
+ for (const [userId, activity] of this.suspiciousActivity.entries()) {
605
+ activity.events = activity.events.filter(event =>
606
+ now - event.timestamp < maxAge
607
+ );
608
+
609
+ activity.count = activity.events.length;
610
+
611
+ if (activity.count === 0) {
612
+ this.suspiciousActivity.delete(userId);
613
+ }
614
+ }
615
+ }
616
+
617
+ // ===== STATISTICS =====
618
+
619
+ /**
620
+ * Get security statistics
621
+ */
622
+ getSecurityStats() {
623
+ const auditLog = this.storage.read.from("security").get("auditLog") || [];
624
+ const blockedUsers = this.storage.read.from("security").get("blockedUsers") || [];
625
+
626
+ const last24h = Date.now() - (24 * 60 * 60 * 1000);
627
+ const recentEvents = auditLog.filter(event => event.timestamp > last24h);
628
+
629
+ return {
630
+ blockedUsers: {
631
+ total: blockedUsers.length,
632
+ active: Array.from(this.blockedUsers).length
633
+ },
634
+ rateLimits: {
635
+ activeUsers: this.rateLimits.size,
636
+ totalChecks: auditLog.filter(e => e.type === 'rate_limit_exceeded').length
637
+ },
638
+ spam: {
639
+ detected: auditLog.filter(e => e.type === 'spam_detected').length,
640
+ last24h: recentEvents.filter(e => e.type === 'spam_detected').length
641
+ },
642
+ suspiciousActivity: {
643
+ activeUsers: this.suspiciousActivity.size,
644
+ totalEvents: auditLog.filter(e => e.type === 'suspicious_activity').length
645
+ },
646
+ auditLog: {
647
+ totalEvents: auditLog.length,
648
+ last24h: recentEvents.length,
649
+ eventTypes: this.getEventTypeDistribution(recentEvents)
650
+ }
651
+ };
652
+ }
653
+
654
+ /**
655
+ * Get event type distribution
656
+ */
657
+ getEventTypeDistribution(events) {
658
+ const distribution = {};
659
+
660
+ events.forEach(event => {
661
+ distribution[event.type] = (distribution[event.type] || 0) + 1;
662
+ });
663
+
664
+ return distribution;
665
+ }
666
+
667
+ /**
668
+ * Export security data
669
+ */
670
+ exportSecurityData() {
671
+ return {
672
+ config: this.config,
673
+ blockedUsers: Array.from(this.blockedUsers),
674
+ auditLog: this.getAuditLog(1000),
675
+ stats: this.getSecurityStats()
676
+ };
677
+ }
678
+ }