@yrpri/api 9.0.95 → 9.0.97

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.
@@ -35,7 +35,7 @@ var changePostCounter = function (req, postId, column, upDown, next) {
35
35
  };
36
36
  //TODO: Refactor this as not to repeate it in controlelrs
37
37
  const addAgentFabricUserToSessionIfNeeded = async (req) => {
38
- let userId = (req.user && req.user.id) ? req.user.id : null;
38
+ let userId = req.user && req.user.id ? req.user.id : null;
39
39
  if (!userId &&
40
40
  req.query.agentFabricUserId &&
41
41
  process.env.PS_TEMP_AGENTS_FABRIC_GROUP_API_KEY &&
@@ -664,7 +664,7 @@ router.get("/:id/newPoints", auth.can("view post"), function (req, res) {
664
664
  model: models.Image,
665
665
  as: "UserProfileImages",
666
666
  required: false,
667
- through: { attributes: [] }
667
+ through: { attributes: [] },
668
668
  },
669
669
  ],
670
670
  },
@@ -842,7 +842,7 @@ const sendPostPoints = (req, res, redisKey) => {
842
842
  as: "UserProfileImages",
843
843
  attributes: ["id", "formats"],
844
844
  required: false,
845
- through: { attributes: [] }
845
+ through: { attributes: [] },
846
846
  },
847
847
  ],
848
848
  },
@@ -1144,10 +1144,18 @@ router.post("/:groupId", auth.can("create post"), async function (req, res) {
1144
1144
  if (!error && post) {
1145
1145
  post.setDataValue("newEndorsement", endorsement);
1146
1146
  log.info("process-moderation post toxicity in post controller");
1147
- queue.add("process-moderation", {
1148
- type: "estimate-post-toxicity",
1149
- postId: post.id,
1150
- }, "high");
1147
+ const skipModerationForAgentFabric = !req.query.skipModerationForAgentFabric &&
1148
+ process.env
1149
+ .PS_TEMP_AGENTS_FABRIC_GROUP_API_KEY &&
1150
+ req.headers["x-api-key"] ===
1151
+ process.env
1152
+ .PS_TEMP_AGENTS_FABRIC_GROUP_API_KEY;
1153
+ if (!skipModerationForAgentFabric) {
1154
+ queue.add("process-moderation", {
1155
+ type: "estimate-post-toxicity",
1156
+ postId: post.id,
1157
+ }, "high");
1158
+ }
1151
1159
  queue.add("process-moderation", {
1152
1160
  type: "post-review-and-annotate-images",
1153
1161
  postId: post.id,
@@ -1849,9 +1857,7 @@ router.delete("/:id/endorse", auth.can("vote on post"), function (req, res) {
1849
1857
  endorsement.save().then(function () {
1850
1858
  if (oldEndorsementValue > 0) {
1851
1859
  changePostCounter(req, req.params.id, "counter_endorsements_up", -1, function () {
1852
- res
1853
- .status(200)
1854
- .send({
1860
+ res.status(200).send({
1855
1861
  endorsement: endorsement,
1856
1862
  oldEndorsementValue: oldEndorsementValue,
1857
1863
  });
@@ -1859,9 +1865,7 @@ router.delete("/:id/endorse", auth.can("vote on post"), function (req, res) {
1859
1865
  }
1860
1866
  else if (oldEndorsementValue < 0) {
1861
1867
  changePostCounter(req, req.params.id, "counter_endorsements_down", -1, function () {
1862
- res
1863
- .status(200)
1864
- .send({
1868
+ res.status(200).send({
1865
1869
  endorsement: endorsement,
1866
1870
  oldEndorsementValue: oldEndorsementValue,
1867
1871
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrpri/api",
3
- "version": "9.0.95",
3
+ "version": "9.0.97",
4
4
  "license": "MIT",
5
5
  "author": "Robert Bjarnason & Citizens Foundation",
6
6
  "repository": {
package/webSockets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { WebSocketServer, WebSocket } from "ws";
1
+ import { WebSocketServer } from "ws";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  /**
4
4
  * WebSocketsManager:
@@ -10,26 +10,15 @@ export class WebSocketsManager {
10
10
  constructor(wsClients, redisClient, server) {
11
11
  // Ping/pong heartbeat interval
12
12
  this.pingInterval = null;
13
- // Configuration for Redis reconnection attempts
14
- this.maxRedisReconnectAttempts = 5;
15
- this.redisReconnectDelay = 2000; // milliseconds
16
13
  this.wsClients = wsClients;
17
14
  this.redisClient = redisClient;
18
15
  this.ws = new WebSocketServer({ server });
19
- // You could also read this from environment or a hostname if preferred
20
- this.serverId =
21
- process.env.NODE_ENV === "development"
22
- ? "dev-server"
23
- : `server-${Math.random()}`;
24
- console.log("WebSockets: Starting WebSocketsManager on serverId:", this.serverId);
25
16
  }
26
17
  /**
27
18
  * Main entry point to start listening for connections
28
19
  * and initialize Redis pub/sub, heartbeat, etc.
29
20
  */
30
21
  async listen() {
31
- // Setup Redis pub/sub with reconnection logic
32
- await this.setupPubSub();
33
22
  // Start periodic ping/pong for WS clients
34
23
  this.startPingCheck();
35
24
  // Convert to async so we can 'await' inside
@@ -40,82 +29,16 @@ export class WebSocketsManager {
40
29
  if (!clientId) {
41
30
  clientId = uuidv4();
42
31
  }
43
- // 2) Attempt to claim ownership of this clientId in Redis
44
- const claimed = await this.tryClaimClientId(clientId);
45
- if (!claimed) {
46
- // Another server owns it—ask that server to release
47
- await this.requestReleaseClientId(clientId);
48
- // Optionally, you might wait or forcibly overwrite after some delay
49
- console.log(`WebSockets: Server ${this.serverId} could not claim clientId ${clientId} immediately.`);
50
- }
51
- else {
52
- console.log(`WebSockets: Server ${this.serverId} claimed clientId ${clientId}.`);
53
- }
54
- // 3) Locally, if there's already a socket for that ID on *this* server, terminate it
55
- const oldSocket = this.wsClients.get(clientId);
56
- if (oldSocket && oldSocket.readyState === WebSocket.OPEN) {
57
- oldSocket.terminate();
58
- }
59
- // 4) Store this new WebSocket in the map
60
32
  this.wsClients.set(clientId, ws);
61
- // 5) Mark it as alive for ping/pong
62
33
  ws.isAlive = true;
63
34
  ws.on("pong", () => {
64
35
  ws.isAlive = true;
65
36
  });
66
- console.log(`WebSockets: New WebSocket connection on serverId ${this.serverId}: clientId ${clientId}`);
67
- // 6) Send the final clientId back to the client
37
+ console.log(`WebSockets: New WebSocket connection: clientId ${clientId}`);
68
38
  ws.send(JSON.stringify({ clientId }));
69
- // 7) Server-level message listener
70
- ws.on("message", (messageData) => {
71
- // If for some reason we *no longer* own this clientId, publish to Redis
72
- // so that whichever server DOES own it can handle the message on a higher level in the code that adds their own .on("message")
73
- if (!this.wsClients.has(clientId)) {
74
- let parsedMessage;
75
- try {
76
- parsedMessage = JSON.parse(messageData.toString());
77
- }
78
- catch (err) {
79
- console.log(`WebSockets: Received non-JSON message from client ${clientId}:`, messageData.toString());
80
- parsedMessage = messageData.toString();
81
- }
82
- const messageToSend = JSON.stringify({
83
- clientId,
84
- action: "directMessage",
85
- data: parsedMessage,
86
- });
87
- this.pub
88
- ?.publish("ypWebsocketChannel", messageToSend)
89
- .then((reply) => {
90
- console.log(`WebSockets: Message published to ypWebsocketChannel: ${reply}`);
91
- })
92
- .catch((err) => {
93
- console.error("WebSockets: Error publishing to Redis:", err);
94
- });
95
- }
96
- else {
97
- // Otherwise, we handle it locally or pass it on
98
- // Typically you'd have local server logic here,
99
- // or forward it to some local "chatbot" logic, etc.
100
- /*console.log(
101
- `WebSockets: Local server ${this.serverId} handling message from clientId ${clientId}`
102
- );*/
103
- }
104
- });
105
- // 8) Clean up on close/error
106
39
  ws.on("close", async () => {
107
- // If we still own the clientId, remove it from local map
108
40
  this.wsClients.delete(clientId);
109
- console.log(`WebSockets: WebSocket closed: clientId ${clientId} on server ${this.serverId}`);
110
- // Optionally, we can remove ownership from Redis if the user disconnected
111
- // But you may only want to do that after some time, or not at all,
112
- // depending on whether you expect reconnections with the same clientId.
113
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
114
- if (currentOwner === this.serverId) {
115
- // We are the legitimate owner, so free the key
116
- await this.redisClient.del(`clientOwner:${clientId}`);
117
- console.log(`WebSockets: Server ${this.serverId} removed ownership for clientId ${clientId} from Redis.`);
118
- }
41
+ console.log(`WebSockets: WebSocket closed: clientId ${clientId}`);
119
42
  });
120
43
  ws.on("error", (err) => {
121
44
  this.wsClients.delete(clientId);
@@ -123,179 +46,6 @@ export class WebSocketsManager {
123
46
  });
124
47
  });
125
48
  }
126
- // -------------------------------
127
- // 1) TRY CLAIMING CLIENT OWNERSHIP
128
- // -------------------------------
129
- async tryClaimClientId(clientId) {
130
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
131
- if (currentOwner === this.serverId) {
132
- // Update expiration if necessary
133
- await this.redisClient.set(`clientOwner:${clientId}`, this.serverId, {
134
- XX: true,
135
- EX: 24 * 60 * 60,
136
- });
137
- return true;
138
- }
139
- // Use SETNX approach
140
- // NX => only set if key does NOT exist
141
- // EX => optional expiry; you can set a day, etc. so it doesn't last forever
142
- const result = await this.redisClient.set(`clientOwner:${clientId}`, this.serverId, {
143
- NX: true, // Set only if it doesn't exist
144
- EX: 24 * 60 * 60, // e.g. 24h, or omit if you prefer no expiry
145
- });
146
- if (result === "OK") {
147
- // We successfully claimed
148
- return true;
149
- }
150
- else {
151
- // Another server owns it
152
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
153
- console.log(`WebSockets: Unable to claim clientId ${clientId}, already owned by ${currentOwner}`);
154
- return false;
155
- }
156
- }
157
- // -------------------------------
158
- // 2) REQUEST RELEASE FROM OLD OWNER
159
- // -------------------------------
160
- async requestReleaseClientId(clientId) {
161
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
162
- if (!currentOwner)
163
- return; // No one actually owns it, maybe it expired?
164
- if (currentOwner === this.serverId) {
165
- // We ironically already own it (possibly race condition?), just return
166
- return;
167
- }
168
- console.log(`WebSockets: Requesting server ${currentOwner} to release clientId ${clientId}`);
169
- const msg = {
170
- action: "releaseClientId",
171
- clientId,
172
- fromServer: this.serverId,
173
- oldOwner: currentOwner,
174
- };
175
- await this.pub?.publish("ypControlChannel", JSON.stringify(msg));
176
- }
177
- // -------------------------------
178
- // 3) SETUP REDIS PUB/SUB + CONTROL CHANNEL
179
- // -------------------------------
180
- async setupPubSub() {
181
- this.pub = this.redisClient.duplicate();
182
- this.sub = this.redisClient.duplicate();
183
- // Listen for errors and attempt to reconnect when needed
184
- this.pub.on("error", (err) => {
185
- console.error("WebSockets: Publisher Redis client error:", err);
186
- this.handleRedisError(this.pub);
187
- });
188
- this.sub.on("error", (err) => {
189
- console.error("WebSockets: Subscriber Redis client error:", err);
190
- this.handleRedisError(this.sub);
191
- });
192
- // Connect with retry logic
193
- await Promise.all([
194
- this.connectRedisClient(this.pub, "Publisher"),
195
- this.connectRedisClient(this.sub, "Subscriber"),
196
- ]);
197
- // 3a) Subscribe to the primary channel for direct messages
198
- this.sub.subscribe("ypWebsocketChannel", (message, channel) => {
199
- try {
200
- const parsed = JSON.parse(message);
201
- const { clientId, action, data } = parsed;
202
- console.log(`WebSockets: Received from Redis on ${channel}: ${JSON.stringify(parsed)}`);
203
- switch (action) {
204
- case "directMessage":
205
- // If we own this clientId, forward to local WebSocket
206
- const ws = this.wsClients.get(clientId);
207
- if (ws) {
208
- try {
209
- ws.send(JSON.stringify(data));
210
- }
211
- catch (err) {
212
- console.error(`WebSockets: Error sending direct message to ${clientId}:`, err);
213
- }
214
- }
215
- else {
216
- // We apparently don't have that client
217
- console.warn(`WebSockets: No WebSocket found locally for clientId ${clientId}`);
218
- this.wsClients.delete(clientId);
219
- }
220
- break;
221
- default:
222
- console.warn(`WebSockets: Unknown action '${action}' received from Redis.`);
223
- }
224
- }
225
- catch (err) {
226
- console.error("WebSockets: Error handling Redis message:", err);
227
- }
228
- });
229
- // 3b) Subscribe to the control channel for ownership requests
230
- this.sub.subscribe("ypControlChannel", async (message, channel) => {
231
- try {
232
- const parsed = JSON.parse(message);
233
- const { action, clientId, oldOwner, fromServer } = parsed;
234
- if (action === "releaseClientId") {
235
- // Check if *we* are actually the old owner
236
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
237
- if (currentOwner === this.serverId && this.serverId === oldOwner) {
238
- // We own this client, so let's release it
239
- console.log(`WebSockets: Server ${this.serverId} is releasing clientId ${clientId} (requested by ${fromServer}).`);
240
- // Close local WebSocket if still open
241
- const ws = this.wsClients.get(clientId);
242
- if (ws && ws.readyState === WebSocket.OPEN) {
243
- ws.close(1000, "Another server claimed ownership");
244
- this.wsClients.delete(clientId);
245
- }
246
- // Remove ownership from Redis
247
- await this.redisClient.del(`clientOwner:${clientId}`);
248
- console.log(`WebSockets: Server ${this.serverId} removed ownership for clientId ${clientId}.`);
249
- }
250
- else {
251
- // We don't own it or the ownership mismatch
252
- // Possibly ignore or log
253
- console.log(`WebSockets: releaseClientId ignored by ${this.serverId}. currentOwner=${currentOwner}, oldOwner=${oldOwner}`);
254
- }
255
- }
256
- }
257
- catch (err) {
258
- console.error("WebSockets: Error handling control message:", err);
259
- }
260
- });
261
- }
262
- /**
263
- * Attempt to connect a Redis client with a few retries.
264
- */
265
- async connectRedisClient(client, clientName) {
266
- let attempts = 0;
267
- while (attempts < this.maxRedisReconnectAttempts) {
268
- try {
269
- attempts++;
270
- await client.connect();
271
- console.log(`WebSockets: ${clientName} connected successfully.`);
272
- return;
273
- }
274
- catch (err) {
275
- console.error(`WebSockets: ${clientName} connection attempt ${attempts} failed:`, err);
276
- await new Promise((resolve) => setTimeout(resolve, this.redisReconnectDelay));
277
- }
278
- }
279
- throw new Error(`WebSockets: ${clientName} failed to connect after ${this.maxRedisReconnectAttempts} attempts`);
280
- }
281
- /**
282
- * Handle Redis errors by attempting to reconnect the client.
283
- */
284
- async handleRedisError(client) {
285
- try {
286
- client.disconnect();
287
- }
288
- catch (err) {
289
- console.error("WebSockets: Error disconnecting Redis client during error handling:", err);
290
- }
291
- // Attempt to reconnect
292
- try {
293
- await this.connectRedisClient(client, "Redis Client");
294
- }
295
- catch (err) {
296
- console.error("WebSockets: Failed to reconnect Redis client:", err);
297
- }
298
- }
299
49
  /**
300
50
  * Ping all clients every 30 seconds. If a client does not respond with 'pong',
301
51
  * we assume it's a stale connection and terminate it.
@@ -324,23 +74,4 @@ export class WebSocketsManager {
324
74
  }
325
75
  });
326
76
  }
327
- /**
328
- * Gracefully shut down the Redis pub/sub clients.
329
- */
330
- async shutdownPubSub() {
331
- try {
332
- console.log("WebSockets: Shutting down Redis pub/sub");
333
- if (this.pub) {
334
- await this.pub.disconnect();
335
- console.log("WebSockets: Publisher disconnected gracefully.");
336
- }
337
- if (this.sub) {
338
- await this.sub.disconnect();
339
- console.log("WebSockets: Subscriber disconnected gracefully.");
340
- }
341
- }
342
- catch (err) {
343
- console.error("WebSockets: Error during Redis pub/sub shutdown:", err);
344
- }
345
- }
346
77
  }