powr-sdk-api 2.4.6 → 2.5.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/routes/chat.js +332 -0
- package/dist/routes/index.js +6 -1
- package/package.json +67 -67
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const {
|
|
6
|
+
ObjectId
|
|
7
|
+
} = require('mongodb');
|
|
8
|
+
const {
|
|
9
|
+
getDb
|
|
10
|
+
} = require('../services/mongo');
|
|
11
|
+
|
|
12
|
+
// Get all conversations for a user
|
|
13
|
+
router.get('/conversations', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const userId = req.user.powrId;
|
|
16
|
+
const userAccess = req.user.access;
|
|
17
|
+
const projectId = req.projectId;
|
|
18
|
+
console.log('Current user ID:', userId, 'Access:', userAccess, 'Project:', projectId);
|
|
19
|
+
const db = await getDb();
|
|
20
|
+
const conversations = await db.collection('conversations').find({
|
|
21
|
+
participants: userId,
|
|
22
|
+
projectId: projectId
|
|
23
|
+
}).toArray();
|
|
24
|
+
|
|
25
|
+
// Get user details for each conversation
|
|
26
|
+
const conversationsWithUsers = await Promise.all(conversations.map(async conv => {
|
|
27
|
+
const otherUserId = conv.participants.find(id => id !== userId);
|
|
28
|
+
console.log('Looking for other user with ID:', otherUserId, 'type:', typeof otherUserId);
|
|
29
|
+
const otherUser = await db.collection('users').findOne({
|
|
30
|
+
_id: new ObjectId(otherUserId)
|
|
31
|
+
}, {
|
|
32
|
+
projection: {
|
|
33
|
+
fullName: 1,
|
|
34
|
+
username: 1,
|
|
35
|
+
avatar: 1
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
console.log('Found other user:', otherUser);
|
|
39
|
+
|
|
40
|
+
// Get last message
|
|
41
|
+
const lastMessage = await db.collection('messages').findOne({
|
|
42
|
+
conversationId: conv._id.toString()
|
|
43
|
+
}, {
|
|
44
|
+
sort: {
|
|
45
|
+
createdAt: -1
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Get unread count
|
|
50
|
+
const unreadCount = await db.collection('messages').countDocuments({
|
|
51
|
+
conversationId: conv._id.toString(),
|
|
52
|
+
senderId: {
|
|
53
|
+
$ne: userId
|
|
54
|
+
},
|
|
55
|
+
read: false
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
id: conv._id.toString(),
|
|
59
|
+
name: (otherUser === null || otherUser === void 0 ? void 0 : otherUser.fullName) || 'Unknown User',
|
|
60
|
+
avatar: otherUser === null || otherUser === void 0 ? void 0 : otherUser.avatar,
|
|
61
|
+
lastMessage: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.content) || '',
|
|
62
|
+
lastMessageTime: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.createdAt) || conv.createdAt,
|
|
63
|
+
unreadCount,
|
|
64
|
+
isOnline: false // Simple implementation - could be enhanced later
|
|
65
|
+
};
|
|
66
|
+
}));
|
|
67
|
+
res.success('Conversations fetched successfully', 200, conversationsWithUsers);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error fetching conversations:', error);
|
|
70
|
+
res.error('Failed to fetch conversations', 500, error);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Get messages for a conversation
|
|
75
|
+
router.get('/conversations/:conversationId/messages', async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const {
|
|
78
|
+
conversationId
|
|
79
|
+
} = req.params;
|
|
80
|
+
const userId = req.user.powrId;
|
|
81
|
+
const projectId = req.projectId;
|
|
82
|
+
const db = await getDb();
|
|
83
|
+
|
|
84
|
+
// Verify user is part of conversation
|
|
85
|
+
const conversation = await db.collection('conversations').findOne({
|
|
86
|
+
_id: new ObjectId(conversationId),
|
|
87
|
+
participants: userId,
|
|
88
|
+
projectId: projectId
|
|
89
|
+
});
|
|
90
|
+
if (!conversation) {
|
|
91
|
+
return res.error('Conversation not found', 404);
|
|
92
|
+
}
|
|
93
|
+
const messages = await db.collection('messages').find({
|
|
94
|
+
conversationId: conversationId
|
|
95
|
+
}).sort({
|
|
96
|
+
createdAt: 1
|
|
97
|
+
}).toArray();
|
|
98
|
+
|
|
99
|
+
// Mark messages as read
|
|
100
|
+
await db.collection('messages').updateMany({
|
|
101
|
+
conversationId: conversationId,
|
|
102
|
+
senderId: {
|
|
103
|
+
$ne: userId
|
|
104
|
+
},
|
|
105
|
+
read: false
|
|
106
|
+
}, {
|
|
107
|
+
$set: {
|
|
108
|
+
read: true
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const messagesWithOwnership = messages.map(msg => ({
|
|
112
|
+
id: msg._id.toString(),
|
|
113
|
+
sender: msg.senderName,
|
|
114
|
+
message: msg.content,
|
|
115
|
+
timestamp: msg.createdAt,
|
|
116
|
+
isOwn: msg.senderId === userId
|
|
117
|
+
}));
|
|
118
|
+
res.success('Messages fetched successfully', 200, messagesWithOwnership);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Error fetching messages:', error);
|
|
121
|
+
res.error('Failed to fetch messages', 500, error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Send a message
|
|
126
|
+
router.post('/conversations/:conversationId/messages', async (req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
const {
|
|
129
|
+
conversationId
|
|
130
|
+
} = req.params;
|
|
131
|
+
const {
|
|
132
|
+
content
|
|
133
|
+
} = req.body;
|
|
134
|
+
const userId = req.user.powrId;
|
|
135
|
+
const projectId = req.projectId;
|
|
136
|
+
if (!content || content.trim() === '') {
|
|
137
|
+
return res.error('Message content is required', 400);
|
|
138
|
+
}
|
|
139
|
+
const db = await getDb();
|
|
140
|
+
|
|
141
|
+
// Verify user is part of conversation
|
|
142
|
+
const conversation = await db.collection('conversations').findOne({
|
|
143
|
+
_id: new ObjectId(conversationId),
|
|
144
|
+
participants: userId,
|
|
145
|
+
projectId: projectId
|
|
146
|
+
});
|
|
147
|
+
if (!conversation) {
|
|
148
|
+
return res.error('Conversation not found', 404);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get current user's name from database
|
|
152
|
+
const currentUser = await db.collection('users').findOne({
|
|
153
|
+
_id: new ObjectId(userId)
|
|
154
|
+
}, {
|
|
155
|
+
projection: {
|
|
156
|
+
fullName: 1
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const message = {
|
|
160
|
+
conversationId: conversationId,
|
|
161
|
+
senderId: userId,
|
|
162
|
+
senderName: (currentUser === null || currentUser === void 0 ? void 0 : currentUser.fullName) || 'Unknown User',
|
|
163
|
+
content: content.trim(),
|
|
164
|
+
createdAt: new Date(),
|
|
165
|
+
read: false,
|
|
166
|
+
projectId: projectId
|
|
167
|
+
};
|
|
168
|
+
const result = await db.collection('messages').insertOne(message);
|
|
169
|
+
|
|
170
|
+
// Update conversation last message
|
|
171
|
+
await db.collection('conversations').updateOne({
|
|
172
|
+
_id: new ObjectId(conversationId)
|
|
173
|
+
}, {
|
|
174
|
+
$set: {
|
|
175
|
+
updatedAt: new Date()
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const responseMessage = {
|
|
179
|
+
id: result.insertedId.toString(),
|
|
180
|
+
sender: message.senderName,
|
|
181
|
+
message: message.content,
|
|
182
|
+
timestamp: message.createdAt,
|
|
183
|
+
isOwn: true
|
|
184
|
+
};
|
|
185
|
+
res.success('Message sent successfully', 201, responseMessage);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('Error sending message:', error);
|
|
188
|
+
res.error('Failed to send message', 500, error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Create a new conversation
|
|
193
|
+
router.post('/conversations', async (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const {
|
|
196
|
+
participantId
|
|
197
|
+
} = req.body;
|
|
198
|
+
const userId = req.user.powrId;
|
|
199
|
+
const userAccess = req.user.access;
|
|
200
|
+
const projectId = req.projectId;
|
|
201
|
+
if (!participantId) {
|
|
202
|
+
return res.error('Participant ID is required', 400);
|
|
203
|
+
}
|
|
204
|
+
const db = await getDb();
|
|
205
|
+
|
|
206
|
+
// Check if user has permission to chat with this participant
|
|
207
|
+
const participant = await db.collection('users').findOne({
|
|
208
|
+
_id: new ObjectId(participantId)
|
|
209
|
+
}, {
|
|
210
|
+
projection: {
|
|
211
|
+
access: 1
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
if (!participant) {
|
|
215
|
+
return res.error('Participant not found', 404);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Validate access permissions
|
|
219
|
+
let hasPermission = false;
|
|
220
|
+
if (userAccess === 100) {
|
|
221
|
+
// Admin can chat with everyone
|
|
222
|
+
hasPermission = true;
|
|
223
|
+
} else if (userAccess === 900) {
|
|
224
|
+
// Client can only chat with admins
|
|
225
|
+
hasPermission = participant.access === 100;
|
|
226
|
+
} else {
|
|
227
|
+
// Employee can chat with employees and admins
|
|
228
|
+
hasPermission = participant.access === 100 || participant.access === null;
|
|
229
|
+
}
|
|
230
|
+
if (!hasPermission) {
|
|
231
|
+
return res.error('You do not have permission to chat with this user', 403);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check if conversation already exists
|
|
235
|
+
const existingConversation = await db.collection('conversations').findOne({
|
|
236
|
+
participants: {
|
|
237
|
+
$all: [userId, participantId]
|
|
238
|
+
},
|
|
239
|
+
projectId: projectId
|
|
240
|
+
});
|
|
241
|
+
if (existingConversation) {
|
|
242
|
+
return res.success('Conversation already exists', 200, {
|
|
243
|
+
id: existingConversation._id.toString(),
|
|
244
|
+
message: 'Conversation already exists'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const conversation = {
|
|
248
|
+
participants: [userId, participantId],
|
|
249
|
+
projectId: projectId,
|
|
250
|
+
createdAt: new Date(),
|
|
251
|
+
updatedAt: new Date()
|
|
252
|
+
};
|
|
253
|
+
const result = await db.collection('conversations').insertOne(conversation);
|
|
254
|
+
res.success('Conversation created successfully', 201, {
|
|
255
|
+
id: result.insertedId.toString(),
|
|
256
|
+
message: 'Conversation created successfully'
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('Error creating conversation:', error);
|
|
260
|
+
res.error('Failed to create conversation', 500, error);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Get users for starting new conversations
|
|
265
|
+
router.get('/users', async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const userId = req.user.powrId;
|
|
268
|
+
const userAccess = req.user.access;
|
|
269
|
+
const projectId = req.projectId;
|
|
270
|
+
console.log('Current user access level:', userAccess, 'Project:', projectId);
|
|
271
|
+
const db = await getDb();
|
|
272
|
+
|
|
273
|
+
// Get users from the current project
|
|
274
|
+
const projectUsers = await db.collection('profiles').find({
|
|
275
|
+
projectId: projectId
|
|
276
|
+
}).toArray();
|
|
277
|
+
const userIds = projectUsers.map(profile => profile.userId);
|
|
278
|
+
let userQuery = {
|
|
279
|
+
_id: {
|
|
280
|
+
$in: userIds.map(id => new ObjectId(id))
|
|
281
|
+
},
|
|
282
|
+
_id: {
|
|
283
|
+
$ne: new ObjectId(userId)
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Apply role-based filtering
|
|
288
|
+
if (userAccess === 100) {
|
|
289
|
+
// Admin can chat with everyone
|
|
290
|
+
console.log('Admin access - can chat with everyone');
|
|
291
|
+
} else if (userAccess === 900) {
|
|
292
|
+
// Client can only chat with admins
|
|
293
|
+
userQuery.access = 100;
|
|
294
|
+
console.log('Client access - can only chat with admins');
|
|
295
|
+
} else {
|
|
296
|
+
// Employee (access null) can chat with employees and admins
|
|
297
|
+
userQuery.$or = [{
|
|
298
|
+
access: 100
|
|
299
|
+
},
|
|
300
|
+
// Admins
|
|
301
|
+
{
|
|
302
|
+
access: null
|
|
303
|
+
} // Employees
|
|
304
|
+
];
|
|
305
|
+
console.log('Employee access - can chat with employees and admins');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Get filtered users
|
|
309
|
+
const users = await db.collection('users').find(userQuery, {
|
|
310
|
+
projection: {
|
|
311
|
+
fullName: 1,
|
|
312
|
+
username: 1,
|
|
313
|
+
avatar: 1,
|
|
314
|
+
_id: 1,
|
|
315
|
+
access: 1
|
|
316
|
+
}
|
|
317
|
+
}).toArray();
|
|
318
|
+
const formattedUsers = users.map(user => ({
|
|
319
|
+
id: user._id.toString(),
|
|
320
|
+
name: user.fullName || 'Unknown User',
|
|
321
|
+
username: user.username,
|
|
322
|
+
avatar: user.avatar,
|
|
323
|
+
access: user.access
|
|
324
|
+
}));
|
|
325
|
+
console.log('Available users for chat:', formattedUsers);
|
|
326
|
+
res.success('Users fetched successfully', 200, formattedUsers);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error('Error fetching users:', error);
|
|
329
|
+
res.error('Failed to fetch users', 500, error);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
module.exports = router;
|
package/dist/routes/index.js
CHANGED
|
@@ -4,6 +4,9 @@ const express = require('express');
|
|
|
4
4
|
const {
|
|
5
5
|
injectProjectId
|
|
6
6
|
} = require('../middleware/projectId');
|
|
7
|
+
const {
|
|
8
|
+
verifyToken
|
|
9
|
+
} = require('../middleware/jwtToken');
|
|
7
10
|
|
|
8
11
|
// Import all route modules
|
|
9
12
|
const commentsRoutes = require('./comments');
|
|
@@ -20,6 +23,7 @@ const authRoutes = require('./auth');
|
|
|
20
23
|
const slidesRoutes = require('./slides');
|
|
21
24
|
const notificationsRoutes = require('./notifications');
|
|
22
25
|
const profilesRoutes = require('./profiles');
|
|
26
|
+
const chatRoutes = require('./chat');
|
|
23
27
|
const createPowrRoutes = (options = {}) => {
|
|
24
28
|
const router = express.Router();
|
|
25
29
|
|
|
@@ -38,7 +42,8 @@ const createPowrRoutes = (options = {}) => {
|
|
|
38
42
|
router.use('/auth', authRoutes);
|
|
39
43
|
router.use('/slides', slidesRoutes);
|
|
40
44
|
router.use('/notifications', notificationsRoutes);
|
|
41
|
-
router.use('/profiles', profilesRoutes);
|
|
45
|
+
router.use('/profiles', verifyToken, profilesRoutes);
|
|
46
|
+
router.use('/chat', verifyToken, chatRoutes);
|
|
42
47
|
return router;
|
|
43
48
|
};
|
|
44
49
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "powr-sdk-api",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Shared API core library for PowrStack projects",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"dist",
|
|
9
|
-
"README.md"
|
|
10
|
-
],
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "jest --passWithNoTests",
|
|
13
|
-
"lint": "eslint .",
|
|
14
|
-
"build": "babel src -d dist",
|
|
15
|
-
"prepare": "npm run build",
|
|
16
|
-
"prepublishOnly": "npm run test"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [
|
|
19
|
-
"api",
|
|
20
|
-
"express",
|
|
21
|
-
"middleware",
|
|
22
|
-
"error-handling",
|
|
23
|
-
"response-formatting",
|
|
24
|
-
"logging",
|
|
25
|
-
"swagger",
|
|
26
|
-
"documentation"
|
|
27
|
-
],
|
|
28
|
-
"author": "
|
|
29
|
-
"license": "ISC",
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/powrstack/powr-sdk-api.git"
|
|
33
|
-
},
|
|
34
|
-
"bugs": {
|
|
35
|
-
"url": "https://github.com/powrstack/powr-sdk-api/issues"
|
|
36
|
-
},
|
|
37
|
-
"homepage": "https://github.com/powrstack/powr-sdk-api#readme",
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@aws-sdk/client-s3": "^3.787.0",
|
|
40
|
-
"@google-cloud/storage": "^7.16.0",
|
|
41
|
-
"express": "^4.18.2",
|
|
42
|
-
"jsonwebtoken": "^9.0.2",
|
|
43
|
-
"swagger-jsdoc": "^6.2.8",
|
|
44
|
-
"swagger-ui-express": "^5.0.0",
|
|
45
|
-
"winston": "^3.17.0",
|
|
46
|
-
"mongodb": "^6.3.0",
|
|
47
|
-
"multer": "^1.4.5-lts.1",
|
|
48
|
-
"bcrypt": "^5.1.1"
|
|
49
|
-
},
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@babel/cli": "^7.23.9",
|
|
52
|
-
"@babel/core": "^7.24.0",
|
|
53
|
-
"@babel/preset-env": "^7.24.0",
|
|
54
|
-
"@types/express": "^4.17.21",
|
|
55
|
-
"@types/swagger-jsdoc": "^6.0.4",
|
|
56
|
-
"@types/swagger-ui-express": "^4.1.6",
|
|
57
|
-
"eslint": "^8.57.0",
|
|
58
|
-
"jest": "^29.7.0",
|
|
59
|
-
"typescript": "^5.3.3"
|
|
60
|
-
},
|
|
61
|
-
"peerDependencies": {
|
|
62
|
-
"express": "^4.18.2"
|
|
63
|
-
},
|
|
64
|
-
"engines": {
|
|
65
|
-
"node": ">=14.0.0"
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "powr-sdk-api",
|
|
3
|
+
"version": "2.5.1",
|
|
4
|
+
"description": "Shared API core library for PowrStack projects",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "jest --passWithNoTests",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"build": "babel src -d dist",
|
|
15
|
+
"prepare": "npm run build",
|
|
16
|
+
"prepublishOnly": "npm run test"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"api",
|
|
20
|
+
"express",
|
|
21
|
+
"middleware",
|
|
22
|
+
"error-handling",
|
|
23
|
+
"response-formatting",
|
|
24
|
+
"logging",
|
|
25
|
+
"swagger",
|
|
26
|
+
"documentation"
|
|
27
|
+
],
|
|
28
|
+
"author": "Lawazia Tech",
|
|
29
|
+
"license": "ISC",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/powrstack/powr-sdk-api.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/powrstack/powr-sdk-api/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/powrstack/powr-sdk-api#readme",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@aws-sdk/client-s3": "^3.787.0",
|
|
40
|
+
"@google-cloud/storage": "^7.16.0",
|
|
41
|
+
"express": "^4.18.2",
|
|
42
|
+
"jsonwebtoken": "^9.0.2",
|
|
43
|
+
"swagger-jsdoc": "^6.2.8",
|
|
44
|
+
"swagger-ui-express": "^5.0.0",
|
|
45
|
+
"winston": "^3.17.0",
|
|
46
|
+
"mongodb": "^6.3.0",
|
|
47
|
+
"multer": "^1.4.5-lts.1",
|
|
48
|
+
"bcrypt": "^5.1.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@babel/cli": "^7.23.9",
|
|
52
|
+
"@babel/core": "^7.24.0",
|
|
53
|
+
"@babel/preset-env": "^7.24.0",
|
|
54
|
+
"@types/express": "^4.17.21",
|
|
55
|
+
"@types/swagger-jsdoc": "^6.0.4",
|
|
56
|
+
"@types/swagger-ui-express": "^4.1.6",
|
|
57
|
+
"eslint": "^8.57.0",
|
|
58
|
+
"jest": "^29.7.0",
|
|
59
|
+
"typescript": "^5.3.3"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"express": "^4.18.2"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=14.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|