@open-mercato/core 0.8.1-develop.7455.1.8f7e53badb → 0.8.1-develop.7458.1.3847638f67
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/modules/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/messages/subscribers/message-notification.ts +13 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveCommunicationChannelsSystemUserId } from "./system-user.js";
|
|
2
|
-
|
|
2
|
+
import { EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE } from "../../messages/lib/messageSourceEntityTypes.js";
|
|
3
3
|
function buildInboundIdempotencyKey(channelId, externalMessageId) {
|
|
4
4
|
return `cc:${channelId}:${externalMessageId}`;
|
|
5
5
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/lib/inbound-message-origin.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveCommunicationChannelsSystemUserId } from './system-user'\
|
|
5
|
-
"mappings": "AACA,SAAS,gDAAgD;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveCommunicationChannelsSystemUserId } from './system-user'\nimport { EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE } from '../../messages/lib/messageSourceEntityTypes'\n\nexport { EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE }\n\n/**\n * Dedup key `ingest-inbound-message` stamps on the platform message it composes,\n * so a retried ingest reuses the first attempt's message instead of duplicating\n * it. The `cc:` prefix also identifies the message as ingest-authored \u2014 see\n * {@link isIngestedInboundMessage}.\n */\nexport function buildInboundIdempotencyKey(channelId: string, externalMessageId: string): string {\n return `cc:${channelId}:${externalMessageId}`\n}\n\ntype InboundMessageOriginInput = {\n message: {\n senderUserId: string\n sourceEntityType?: string | null\n idempotencyKey?: string | null\n }\n /** The `MessageChannelLink` already loaded for this message, if any. */\n existingLink: { direction: 'inbound' | 'outbound' } | null | undefined\n tenantId: string\n}\n\n/**\n * Whether a platform message in a channel-linked thread is the one\n * `ingest-inbound-message` composed for an incoming message \u2014 as opposed to a\n * message an operator wrote in the same conversation.\n *\n * The distinction matters because the messages module emits\n * `messages.message.sent` for both, and only the operator's message may be\n * delivered outbound. Before #5535 the discriminator was `sourceEntityType`\n * alone, which every operator reply inherits from the message it answers \u2014 so\n * operator replies were silently dropped along with the ingested messages.\n *\n * Three signals, checked cheapest-first, because the ingested message's inbound\n * `MessageChannelLink` is written *after* the compose command emits its event\n * and is therefore not guaranteed to be visible yet:\n *\n * 1. an inbound `MessageChannelLink` for this message \u2014 authoritative;\n * 2. the `cc:` ingest dedup key ({@link buildInboundIdempotencyKey}) \u2014 set\n * whenever the provider supplied an external message id;\n * 3. the tenant's channel system user as the sender \u2014 the ingest attribution\n * when the conversation has no assigned user.\n */\nexport async function isIngestedInboundMessage(\n em: EntityManager,\n { message, existingLink, tenantId }: InboundMessageOriginInput,\n): Promise<boolean> {\n if (message.sourceEntityType !== EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE) return false\n if (existingLink?.direction === 'inbound') return true\n if (typeof message.idempotencyKey === 'string' && message.idempotencyKey.startsWith('cc:')) return true\n const systemUserId = await resolveCommunicationChannelsSystemUserId(em, tenantId)\n return message.senderUserId === systemUserId\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,gDAAgD;AACzD,SAAS,gDAAgD;AAUlD,SAAS,2BAA2B,WAAmB,mBAAmC;AAC/F,SAAO,MAAM,SAAS,IAAI,iBAAiB;AAC7C;AAkCA,eAAsB,yBACpB,IACA,EAAE,SAAS,cAAc,SAAS,GAChB;AAClB,MAAI,QAAQ,qBAAqB,yCAA0C,QAAO;AAClF,MAAI,cAAc,cAAc,UAAW,QAAO;AAClD,MAAI,OAAO,QAAQ,mBAAmB,YAAY,QAAQ,eAAe,WAAW,KAAK,EAAG,QAAO;AACnG,QAAM,eAAe,MAAM,yCAAyC,IAAI,QAAQ;AAChF,SAAO,QAAQ,iBAAiB;AAClC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { User } from "../../auth/data/entities.js";
|
|
9
9
|
import { Message } from "../data/entities.js";
|
|
10
10
|
import { notificationTypes } from "../notifications.js";
|
|
11
|
+
import { resolveMessageChannelThreadAccess } from "../lib/channelThreadAccess.js";
|
|
11
12
|
import {
|
|
12
13
|
EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,
|
|
13
14
|
SEND_AS_USER_SOURCE_ENTITY_TYPE
|
|
@@ -84,7 +85,18 @@ async function isChannelRoutedMessage(payload, ctx) {
|
|
|
84
85
|
}
|
|
85
86
|
);
|
|
86
87
|
if (!message) return false;
|
|
87
|
-
|
|
88
|
+
if (message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE) return true;
|
|
89
|
+
if (message.sourceEntityType !== EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE) return false;
|
|
90
|
+
const channelThreadAccess = await resolveMessageChannelThreadAccess(
|
|
91
|
+
ctx,
|
|
92
|
+
{
|
|
93
|
+
tenantId: payload.tenantId,
|
|
94
|
+
organizationId: payload.organizationId ?? null
|
|
95
|
+
},
|
|
96
|
+
{ messageThreadId: message.threadId },
|
|
97
|
+
{ userId: null, features: [] }
|
|
98
|
+
);
|
|
99
|
+
return channelThreadAccess !== null;
|
|
88
100
|
} catch {
|
|
89
101
|
return false;
|
|
90
102
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/subscribers/message-notification.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createQueue } from '@open-mercato/queue'\nimport { buildBatchNotificationFromType } from '../../notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '../../notifications/lib/notificationService'\nimport {\n MESSAGES_EMAIL_QUEUE_NAME,\n type SendMessageEmailJob,\n} from '../workers/send-email.worker'\nimport { User } from '../../auth/data/entities'\nimport { Message } from '../data/entities'\nimport { notificationTypes } from '../notifications'\nimport {\n EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,\n SEND_AS_USER_SOURCE_ENTITY_TYPE,\n} from '../lib/messageSourceEntityTypes'\n\nexport const metadata = {\n event: 'messages.message.sent',\n persistent: true,\n id: 'messages:queue-email-delivery',\n}\n\ntype MessageSentPayload = {\n messageId: string\n senderUserId: string\n recipientUserIds: string[]\n sendViaEmail: boolean\n /** Set by compose for channel-ingested messages (#6093); see `resolveNotificationVariables`. */\n inboundFromChannel?: boolean\n externalEmail?: string | null\n forwardedFrom?: string\n replyTo?: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nasync function resolveNotificationVariables(payload: MessageSentPayload, ctx: ResolverContext): Promise<{ title: string; from: string }> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return { title: '', from: '' }\n\n const scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n }\n const whereUser: { id: string; tenantId: string; deletedAt: null; organizationId?: string | null } = {\n id: payload.senderUserId,\n tenantId: payload.tenantId,\n deletedAt: null,\n }\n if (payload.organizationId !== undefined) {\n whereUser.organizationId = payload.organizationId\n }\n\n const [message, sender] = await Promise.all([\n findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n ),\n findOneWithDecryption(\n em,\n User,\n whereUser,\n undefined,\n scope,\n ),\n ])\n\n // #6093: a channel-ingested message is composed on behalf of the channel's\n // system user (or, without one, the assignee), so `senderUserId` is not who\n // wrote it. The correspondent is on the message itself.\n const externalFrom = payload.inboundFromChannel\n ? [message?.externalName, message?.externalEmail].find(\n (value): value is string => typeof value === 'string' && value.trim().length > 0,\n )\n : undefined\n\n return {\n title: typeof message?.subject === 'string' ? message.subject : '',\n from: externalFrom\n ?? (typeof sender?.name === 'string' && sender.name.trim().length > 0\n ? sender.name\n : typeof sender?.email === 'string'\n ? sender.email\n : ''),\n }\n } catch {\n return { title: '', from: '' }\n }\n}\n\n\n/**\n * Whether this Message is already being delivered through a connected\n * channel (`sendAsUser`, or the platform copy of an inbound channel message),\n * so the external-email job below would duplicate \u2014 or, for the inbound\n * case, misdirect \u2014 the send. Mirrors `outbound-bridge.ts`'s re-fetch: the\n * `messages.message.sent` payload doesn't carry `sourceEntityType` and never\n * should, to keep this subscriber decoupled from `communication_channels`.\n *\n * Fails open (`false`, i.e. today's behavior) when the Message can't be\n * found \u2014 we can only skip the external send when we can affirmatively show\n * it is channel-routed.\n */\nasync function isChannelRoutedMessage(payload: MessageSentPayload, ctx: ResolverContext): Promise<boolean> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return false\n\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n },\n )\n if (!message) return false\n
|
|
5
|
-
"mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,mBAAmB;AAC5B,SAAS,sCAAsC;AAC/C,SAAS,kCAAkC;AAC3C;AAAA,EACE;AAAA,OAEK;AACP,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAoBA,eAAe,6BAA6B,SAA6B,KAAgE;AACvI,MAAI;AACF,UAAM,KAAK,IAAI,QAAuB,IAAI,GAAG,KAAK;AAClD,QAAI,CAAC,GAAI,QAAO,EAAE,OAAO,IAAI,MAAM,GAAG;AAEtC,UAAM,QAAQ;AAAA,MACZ,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ,kBAAkB;AAAA,IAC5C;AACA,UAAM,YAA+F;AAAA,MACnG,IAAI,QAAQ;AAAA,MACZ,UAAU,QAAQ;AAAA,MAClB,WAAW;AAAA,IACb;AACA,QAAI,QAAQ,mBAAmB,QAAW;AACxC,gBAAU,iBAAiB,QAAQ;AAAA,IACrC;AAEA,UAAM,CAAC,SAAS,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC1C;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,UACE,IAAI,QAAQ;AAAA,UACZ,UAAU,QAAQ;AAAA,UAClB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAKD,UAAM,eAAe,QAAQ,qBACzB,CAAC,SAAS,cAAc,SAAS,aAAa,EAAE;AAAA,MAC9C,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS;AAAA,IACjF,IACA;AAEJ,WAAO;AAAA,MACL,OAAO,OAAO,SAAS,YAAY,WAAW,QAAQ,UAAU;AAAA,MAChE,MAAM,iBACA,OAAO,QAAQ,SAAS,YAAY,OAAO,KAAK,KAAK,EAAE,SAAS,IAChE,OAAO,OACP,OAAO,QAAQ,UAAU,WACvB,OAAO,QACP;AAAA,IACV;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,OAAO,IAAI,MAAM,GAAG;AAAA,EAC/B;AACF;AAeA,eAAe,uBAAuB,SAA6B,KAAwC;AACzG,MAAI;AACF,UAAM,KAAK,IAAI,QAAuB,IAAI,GAAG,KAAK;AAClD,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,QACE,IAAI,QAAQ;AAAA,QACZ,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ,kBAAkB;AAAA,QAC1C,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,QACE,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ,kBAAkB;AAAA,MAC5C;AAAA,IACF;AACA,QAAI,CAAC,QAAS,QAAO;AACrB,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createQueue } from '@open-mercato/queue'\nimport { buildBatchNotificationFromType } from '../../notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '../../notifications/lib/notificationService'\nimport {\n MESSAGES_EMAIL_QUEUE_NAME,\n type SendMessageEmailJob,\n} from '../workers/send-email.worker'\nimport { User } from '../../auth/data/entities'\nimport { Message } from '../data/entities'\nimport { notificationTypes } from '../notifications'\nimport { resolveMessageChannelThreadAccess } from '../lib/channelThreadAccess'\nimport {\n EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,\n SEND_AS_USER_SOURCE_ENTITY_TYPE,\n} from '../lib/messageSourceEntityTypes'\n\nexport const metadata = {\n event: 'messages.message.sent',\n persistent: true,\n id: 'messages:queue-email-delivery',\n}\n\ntype MessageSentPayload = {\n messageId: string\n senderUserId: string\n recipientUserIds: string[]\n sendViaEmail: boolean\n /** Set by compose for channel-ingested messages (#6093); see `resolveNotificationVariables`. */\n inboundFromChannel?: boolean\n externalEmail?: string | null\n forwardedFrom?: string\n replyTo?: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nasync function resolveNotificationVariables(payload: MessageSentPayload, ctx: ResolverContext): Promise<{ title: string; from: string }> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return { title: '', from: '' }\n\n const scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n }\n const whereUser: { id: string; tenantId: string; deletedAt: null; organizationId?: string | null } = {\n id: payload.senderUserId,\n tenantId: payload.tenantId,\n deletedAt: null,\n }\n if (payload.organizationId !== undefined) {\n whereUser.organizationId = payload.organizationId\n }\n\n const [message, sender] = await Promise.all([\n findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n ),\n findOneWithDecryption(\n em,\n User,\n whereUser,\n undefined,\n scope,\n ),\n ])\n\n // #6093: a channel-ingested message is composed on behalf of the channel's\n // system user (or, without one, the assignee), so `senderUserId` is not who\n // wrote it. The correspondent is on the message itself.\n const externalFrom = payload.inboundFromChannel\n ? [message?.externalName, message?.externalEmail].find(\n (value): value is string => typeof value === 'string' && value.trim().length > 0,\n )\n : undefined\n\n return {\n title: typeof message?.subject === 'string' ? message.subject : '',\n from: externalFrom\n ?? (typeof sender?.name === 'string' && sender.name.trim().length > 0\n ? sender.name\n : typeof sender?.email === 'string'\n ? sender.email\n : ''),\n }\n } catch {\n return { title: '', from: '' }\n }\n}\n\n\n/**\n * Whether this Message is already being delivered through a connected\n * channel (`sendAsUser`, or the platform copy of an inbound channel message),\n * so the external-email job below would duplicate \u2014 or, for the inbound\n * case, misdirect \u2014 the send. Mirrors `outbound-bridge.ts`'s re-fetch: the\n * `messages.message.sent` payload doesn't carry `sourceEntityType` and never\n * should, to keep this subscriber decoupled from `communication_channels`.\n *\n * Fails open (`false`, i.e. today's behavior) when the Message can't be\n * found \u2014 we can only skip the external send when we can affirmatively show\n * it is channel-routed.\n */\nasync function isChannelRoutedMessage(payload: MessageSentPayload, ctx: ResolverContext): Promise<boolean> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return false\n\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n },\n )\n if (!message) return false\n if (message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE) return true\n if (message.sourceEntityType !== EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE) return false\n\n const channelThreadAccess = await resolveMessageChannelThreadAccess(\n ctx,\n {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n },\n { messageThreadId: message.threadId },\n { userId: null, features: [] },\n )\n return channelThreadAccess !== null\n } catch {\n return false\n }\n}\n\nexport default async function handle(payload: MessageSentPayload, ctx: ResolverContext): Promise<void> {\n const uniqueRecipientUserIds = Array.from(new Set(payload.recipientUserIds))\n\n const typeDef = notificationTypes.find((type) => type.type === 'messages.new')\n if (typeDef && uniqueRecipientUserIds.length > 0) {\n const variables = await resolveNotificationVariables(payload, ctx)\n const notificationService = resolveNotificationService(ctx)\n const notificationInput = buildBatchNotificationFromType(typeDef, {\n recipientUserIds: uniqueRecipientUserIds,\n titleVariables: variables,\n bodyVariables: variables,\n sourceEntityType: 'message',\n sourceEntityId: payload.messageId,\n linkHref: `/backend/messages/${payload.messageId}`,\n })\n await notificationService.createBatch(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n\n if (!payload.sendViaEmail) {\n return\n }\n\n const strategy = process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n\n const emailQueue = createQueue<SendMessageEmailJob>(MESSAGES_EMAIL_QUEUE_NAME, strategy)\n\n for (const recipientUserId of uniqueRecipientUserIds) {\n await emailQueue.enqueue({\n type: 'recipient',\n messageId: payload.messageId,\n recipientUserId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n\n const externalEmail = payload.externalEmail?.trim()\n if (externalEmail && !(await isChannelRoutedMessage(payload, ctx))) {\n await emailQueue.enqueue({\n type: 'external',\n messageId: payload.messageId,\n email: externalEmail,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.8.1-develop.
|
|
3
|
+
"version": "0.8.1-develop.7458.1.3847638f67",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -264,16 +264,16 @@
|
|
|
264
264
|
"zod": "^4.4.3"
|
|
265
265
|
},
|
|
266
266
|
"peerDependencies": {
|
|
267
|
-
"@open-mercato/ai-assistant": "0.8.1-develop.
|
|
268
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
269
|
-
"@open-mercato/ui": "0.8.1-develop.
|
|
267
|
+
"@open-mercato/ai-assistant": "0.8.1-develop.7458.1.3847638f67",
|
|
268
|
+
"@open-mercato/shared": "0.8.1-develop.7458.1.3847638f67",
|
|
269
|
+
"@open-mercato/ui": "0.8.1-develop.7458.1.3847638f67",
|
|
270
270
|
"react": "^19.0.0",
|
|
271
271
|
"react-dom": "^19.0.0"
|
|
272
272
|
},
|
|
273
273
|
"devDependencies": {
|
|
274
|
-
"@open-mercato/ai-assistant": "0.8.1-develop.
|
|
275
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
276
|
-
"@open-mercato/ui": "0.8.1-develop.
|
|
274
|
+
"@open-mercato/ai-assistant": "0.8.1-develop.7458.1.3847638f67",
|
|
275
|
+
"@open-mercato/shared": "0.8.1-develop.7458.1.3847638f67",
|
|
276
|
+
"@open-mercato/ui": "0.8.1-develop.7458.1.3847638f67",
|
|
277
277
|
"@testing-library/dom": "^10.4.1",
|
|
278
278
|
"@testing-library/jest-dom": "^7.0.1",
|
|
279
279
|
"@testing-library/react": "^16.3.3",
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { resolveCommunicationChannelsSystemUserId } from './system-user'
|
|
3
|
+
import { EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE } from '../../messages/lib/messageSourceEntityTypes'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
* Entity type a channel-backed conversation is exposed under on a message's
|
|
6
|
-
* `sourceEntityType`. Written by `commands/ingest-inbound-message.ts` and
|
|
7
|
-
* inherited by every operator message composed on the same conversation.
|
|
8
|
-
*/
|
|
9
|
-
export const EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE = 'communication_channels.external_conversation'
|
|
5
|
+
export { EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE }
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* Dedup key `ingest-inbound-message` stamps on the platform message it composes,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { User } from '../../auth/data/entities'
|
|
11
11
|
import { Message } from '../data/entities'
|
|
12
12
|
import { notificationTypes } from '../notifications'
|
|
13
|
+
import { resolveMessageChannelThreadAccess } from '../lib/channelThreadAccess'
|
|
13
14
|
import {
|
|
14
15
|
EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,
|
|
15
16
|
SEND_AS_USER_SOURCE_ENTITY_TYPE,
|
|
@@ -135,10 +136,19 @@ async function isChannelRoutedMessage(payload: MessageSentPayload, ctx: Resolver
|
|
|
135
136
|
},
|
|
136
137
|
)
|
|
137
138
|
if (!message) return false
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
if (message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE) return true
|
|
140
|
+
if (message.sourceEntityType !== EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE) return false
|
|
141
|
+
|
|
142
|
+
const channelThreadAccess = await resolveMessageChannelThreadAccess(
|
|
143
|
+
ctx,
|
|
144
|
+
{
|
|
145
|
+
tenantId: payload.tenantId,
|
|
146
|
+
organizationId: payload.organizationId ?? null,
|
|
147
|
+
},
|
|
148
|
+
{ messageThreadId: message.threadId },
|
|
149
|
+
{ userId: null, features: [] },
|
|
141
150
|
)
|
|
151
|
+
return channelThreadAccess !== null
|
|
142
152
|
} catch {
|
|
143
153
|
return false
|
|
144
154
|
}
|