@invago/mixin 1.0.16 → 1.0.17

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.
@@ -2,7 +2,7 @@
2
2
  "id": "mixin",
3
3
  "name": "Mixin Messenger Channel",
4
4
  "description": "Mixin Messenger channel via Blaze WebSocket",
5
- "version": "1.0.16",
5
+ "version": "1.0.17",
6
6
  "channels": [
7
7
  "mixin"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invago/mixin",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Mixin Messenger channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -29,8 +29,9 @@ export interface MixinInboundMessage {
29
29
  publicKey?: string;
30
30
  }
31
31
 
32
- const processedMessages = new Set<string>();
33
- const MAX_DEDUP_SIZE = 2000;
32
+ const processedMessages = new Set<string>();
33
+ const processingMessages = new Set<string>();
34
+ const MAX_DEDUP_SIZE = 2000;
34
35
  const unauthNotifiedUsers = new Map<string, number>();
35
36
  const unauthNotifiedGroups = new Map<string, number>();
36
37
  const loggedAllowFromAccounts = new Set<string>();
@@ -161,19 +162,28 @@ function evaluateMixinSenderGroupAccess(params: {
161
162
  };
162
163
  }
163
164
 
164
- function isProcessed(messageId: string): boolean {
165
- return processedMessages.has(messageId);
166
- }
167
-
168
- function markProcessed(messageId: string): void {
169
- if (processedMessages.size >= MAX_DEDUP_SIZE) {
170
- const first = processedMessages.values().next().value;
171
- if (first) {
172
- processedMessages.delete(first);
165
+ function markProcessing(messageId: string): boolean {
166
+ if (processedMessages.has(messageId) || processingMessages.has(messageId)) {
167
+ return false;
168
+ }
169
+ processingMessages.add(messageId);
170
+ return true;
171
+ }
172
+
173
+ function markProcessed(messageId: string): void {
174
+ processingMessages.delete(messageId);
175
+ if (processedMessages.size >= MAX_DEDUP_SIZE) {
176
+ const first = processedMessages.values().next().value;
177
+ if (first) {
178
+ processedMessages.delete(first);
173
179
  }
174
180
  }
175
- processedMessages.add(messageId);
176
- }
181
+ processedMessages.add(messageId);
182
+ }
183
+
184
+ function clearProcessing(messageId: string): void {
185
+ processingMessages.delete(messageId);
186
+ }
177
187
 
178
188
  function pruneUnauthNotifiedUsers(now: number): void {
179
189
  for (const [userId, lastNotified] of unauthNotifiedUsers) {
@@ -980,76 +990,77 @@ function evaluateMixinGroupAccess(params: {
980
990
  };
981
991
  }
982
992
 
983
- export async function handleMixinMessage(params: {
993
+ export async function handleMixinMessage(params: {
984
994
  cfg: OpenClawConfig;
985
995
  accountId: string;
986
996
  msg: MixinInboundMessage;
987
997
  isDirect: boolean;
988
998
  log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
989
999
  }): Promise<void> {
990
- const { cfg, accountId, msg, isDirect, log } = params;
991
- const rt = getMixinRuntime();
992
-
993
- if (isProcessed(msg.messageId)) {
994
- return;
995
- }
996
-
997
- const config = getAccountConfig(cfg, accountId);
998
-
999
- if (msg.category === "ENCRYPTED_TEXT" || msg.category === "ENCRYPTED_POST") {
1000
- log.info(`[mixin] decrypting encrypted message ${msg.messageId}, category=${msg.category}`);
1001
- try {
1002
- const decrypted = decryptMixinMessage(
1003
- msg.data,
1004
- config.sessionPrivateKey!,
1005
- config.sessionId!,
1006
- );
1007
- if (!decrypted) {
1008
- log.error(`[mixin] decryption failed for ${msg.messageId}`);
1009
- markProcessed(msg.messageId);
1010
- return;
1011
- }
1012
- log.info(`[mixin] decryption successful: messageId=${msg.messageId}, length=${decrypted.length}`);
1013
- msg.data = Buffer.from(decrypted).toString("base64");
1014
- msg.category = "PLAIN_TEXT";
1015
- } catch (err) {
1016
- log.error(`[mixin] decryption exception for ${msg.messageId}`, err);
1017
- markProcessed(msg.messageId);
1018
- return;
1019
- }
1020
- }
1021
-
1022
- let isTextMessage = msg.category.startsWith("PLAIN_TEXT") || msg.category.startsWith("PLAIN_POST");
1023
- const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
1000
+ const { cfg, accountId, msg, isDirect, log } = params;
1001
+ const rt = getMixinRuntime();
1024
1002
 
1025
- if (!isTextMessage && !isAttachmentMessage) {
1026
- const fallbackText = tryDecodeFallbackText(msg.data);
1027
- if (fallbackText) {
1028
- log.warn(
1029
- `[mixin] treating unexpected category as text: messageId=${msg.messageId}, category=${msg.category}, fallbackLength=${fallbackText.length}`,
1030
- );
1031
- msg.category = "PLAIN_TEXT";
1032
- msg.data = Buffer.from(fallbackText).toString("base64");
1033
- isTextMessage = true;
1034
- }
1035
- }
1036
-
1037
- if (!isTextMessage && !isAttachmentMessage) {
1038
- log.info(
1039
- `[mixin] skip non-text message: messageId=${msg.messageId}, category=${msg.category}, quoteMessageId=${msg.quoteMessageId ?? "none"}`,
1040
- );
1003
+ if (!markProcessing(msg.messageId)) {
1041
1004
  return;
1042
1005
  }
1043
1006
 
1044
- const decodedBody = decodeContent(msg.category, msg.data);
1045
- let text = decodedBody.trim();
1046
- let mediaPayload: AgentMediaPayload | undefined;
1047
- if (isAttachmentMessage) {
1048
- const resolved = await resolveInboundAttachment({ rt, config, msg, log });
1049
- text = resolved.text.trim();
1050
- mediaPayload = resolved.mediaPayload;
1051
- }
1052
- log.info(`[mixin] decoded text: messageId=${msg.messageId}, category=${msg.category}, length=${text.length}`);
1007
+ try {
1008
+ const config = getAccountConfig(cfg, accountId);
1009
+
1010
+ if (msg.category === "ENCRYPTED_TEXT" || msg.category === "ENCRYPTED_POST") {
1011
+ log.info(`[mixin] decrypting encrypted message ${msg.messageId}, category=${msg.category}`);
1012
+ try {
1013
+ const decrypted = decryptMixinMessage(
1014
+ msg.data,
1015
+ config.sessionPrivateKey!,
1016
+ config.sessionId!,
1017
+ );
1018
+ if (!decrypted) {
1019
+ log.error(`[mixin] decryption failed for ${msg.messageId}`);
1020
+ markProcessed(msg.messageId);
1021
+ return;
1022
+ }
1023
+ log.info(`[mixin] decryption successful: messageId=${msg.messageId}, length=${decrypted.length}`);
1024
+ msg.data = Buffer.from(decrypted).toString("base64");
1025
+ msg.category = "PLAIN_TEXT";
1026
+ } catch (err) {
1027
+ log.error(`[mixin] decryption exception for ${msg.messageId}`, err);
1028
+ markProcessed(msg.messageId);
1029
+ return;
1030
+ }
1031
+ }
1032
+
1033
+ let isTextMessage = msg.category.startsWith("PLAIN_TEXT") || msg.category.startsWith("PLAIN_POST");
1034
+ const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
1035
+
1036
+ if (!isTextMessage && !isAttachmentMessage) {
1037
+ const fallbackText = tryDecodeFallbackText(msg.data);
1038
+ if (fallbackText) {
1039
+ log.warn(
1040
+ `[mixin] treating unexpected category as text: messageId=${msg.messageId}, category=${msg.category}, fallbackLength=${fallbackText.length}`,
1041
+ );
1042
+ msg.category = "PLAIN_TEXT";
1043
+ msg.data = Buffer.from(fallbackText).toString("base64");
1044
+ isTextMessage = true;
1045
+ }
1046
+ }
1047
+
1048
+ if (!isTextMessage && !isAttachmentMessage) {
1049
+ log.info(
1050
+ `[mixin] skip non-text message: messageId=${msg.messageId}, category=${msg.category}, quoteMessageId=${msg.quoteMessageId ?? "none"}`,
1051
+ );
1052
+ return;
1053
+ }
1054
+
1055
+ const decodedBody = decodeContent(msg.category, msg.data);
1056
+ let text = decodedBody.trim();
1057
+ let mediaPayload: AgentMediaPayload | undefined;
1058
+ if (isAttachmentMessage) {
1059
+ const resolved = await resolveInboundAttachment({ rt, config, msg, log });
1060
+ text = resolved.text.trim();
1061
+ mediaPayload = resolved.mediaPayload;
1062
+ }
1063
+ log.info(`[mixin] decoded text: messageId=${msg.messageId}, category=${msg.category}, length=${text.length}`);
1053
1064
 
1054
1065
  const botIdentity = await resolveBotIdentity({
1055
1066
  accountId,
@@ -1397,26 +1408,31 @@ export async function handleMixinMessage(params: {
1397
1408
 
1398
1409
  log.info(`[mixin] dispatching ${msg.messageId} from ${msg.userId}`);
1399
1410
 
1400
- await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1401
- ctx,
1402
- cfg,
1403
- dispatcherOptions: {
1404
- deliver: async (payload) => {
1405
- const replyText = payload.text ?? "";
1406
- if (!replyText) {
1407
- return;
1408
- }
1409
- const recipientId = isDirect ? msg.userId : undefined;
1410
- await deliverMixinReply({
1411
- cfg,
1412
- accountId,
1413
- conversationId: msg.conversationId,
1414
- recipientId,
1415
- creatorId: msg.userId,
1416
- text: replyText,
1417
- log,
1418
- });
1419
- },
1420
- },
1421
- });
1422
- }
1411
+ await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1412
+ ctx,
1413
+ cfg,
1414
+ dispatcherOptions: {
1415
+ deliver: async (payload) => {
1416
+ const replyText = payload.text ?? "";
1417
+ if (!replyText) {
1418
+ return;
1419
+ }
1420
+ const recipientId = isDirect ? msg.userId : undefined;
1421
+ await deliverMixinReply({
1422
+ cfg,
1423
+ accountId,
1424
+ conversationId: msg.conversationId,
1425
+ recipientId,
1426
+ creatorId: msg.userId,
1427
+ text: replyText,
1428
+ log,
1429
+ });
1430
+ },
1431
+ },
1432
+ });
1433
+ } finally {
1434
+ if (!processedMessages.has(msg.messageId)) {
1435
+ clearProcessing(msg.messageId);
1436
+ }
1437
+ }
1438
+ }