feat(chat): add contact inbox sources with per-user hide filters
This commit is contained in:
@@ -79,6 +79,8 @@ model Team {
|
||||
feedCards FeedCard[]
|
||||
contactPins ContactPin[]
|
||||
documents WorkspaceDocument[]
|
||||
contactInboxes ContactInbox[]
|
||||
contactInboxPreferences ContactInboxPreference[]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -93,6 +95,7 @@ model User {
|
||||
memberships TeamMember[]
|
||||
aiConversations AiConversation[] @relation("ConversationCreator")
|
||||
aiMessages AiMessage[] @relation("ChatAuthor")
|
||||
contactInboxPreferences ContactInboxPreference[]
|
||||
}
|
||||
|
||||
model TeamMember {
|
||||
@@ -133,6 +136,7 @@ model Contact {
|
||||
omniThreads OmniThread[]
|
||||
omniMessages OmniMessage[]
|
||||
omniIdentities OmniContactIdentity[]
|
||||
contactInboxes ContactInbox[]
|
||||
|
||||
@@index([teamId, updatedAt])
|
||||
}
|
||||
@@ -150,6 +154,7 @@ model ContactNote {
|
||||
model ContactMessage {
|
||||
id String @id @default(cuid())
|
||||
contactId String
|
||||
contactInboxId String?
|
||||
kind ContactMessageKind @default(MESSAGE)
|
||||
direction MessageDirection
|
||||
channel MessageChannel
|
||||
@@ -160,9 +165,48 @@ model ContactMessage {
|
||||
occurredAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
contactInbox ContactInbox? @relation(fields: [contactInboxId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([contactId, occurredAt])
|
||||
@@index([contactInboxId, occurredAt])
|
||||
}
|
||||
|
||||
model ContactInbox {
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
contactId String
|
||||
channel MessageChannel
|
||||
sourceExternalId String
|
||||
title String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
messages ContactMessage[]
|
||||
preferences ContactInboxPreference[]
|
||||
|
||||
@@unique([teamId, channel, sourceExternalId])
|
||||
@@index([contactId, updatedAt])
|
||||
@@index([teamId, updatedAt])
|
||||
}
|
||||
|
||||
model ContactInboxPreference {
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
userId String
|
||||
contactInboxId String
|
||||
isHidden Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
contactInbox ContactInbox @relation(fields: [contactInboxId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, contactInboxId])
|
||||
@@index([teamId, userId, isHidden])
|
||||
}
|
||||
|
||||
model OmniContactIdentity {
|
||||
|
||||
@@ -242,7 +242,7 @@ async function upsertThread(input: {
|
||||
|
||||
if (existing) {
|
||||
const data: Prisma.OmniThreadUpdateInput = {
|
||||
contactId: input.contactId,
|
||||
contact: { connect: { id: input.contactId } },
|
||||
};
|
||||
if (input.title && !existing.title) {
|
||||
data.title = input.title;
|
||||
@@ -283,12 +283,42 @@ async function upsertThread(input: {
|
||||
|
||||
await prisma.omniThread.update({
|
||||
where: { id: concurrentThread.id },
|
||||
data: { contactId: input.contactId },
|
||||
data: { contact: { connect: { id: input.contactId } } },
|
||||
});
|
||||
return concurrentThread;
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertContactInbox(input: {
|
||||
teamId: string;
|
||||
contactId: string;
|
||||
channel: "TELEGRAM";
|
||||
sourceExternalId: string;
|
||||
title: string | null;
|
||||
}) {
|
||||
return prisma.contactInbox.upsert({
|
||||
where: {
|
||||
teamId_channel_sourceExternalId: {
|
||||
teamId: input.teamId,
|
||||
channel: input.channel,
|
||||
sourceExternalId: input.sourceExternalId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
teamId: input.teamId,
|
||||
contactId: input.contactId,
|
||||
channel: input.channel,
|
||||
sourceExternalId: input.sourceExternalId,
|
||||
title: input.title,
|
||||
},
|
||||
update: {
|
||||
contactId: input.contactId,
|
||||
...(input.title ? { title: input.title } : {}),
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
async function ingestInbound(env: OmniInboundEnvelopeV1) {
|
||||
if (env.channel !== "TELEGRAM") return;
|
||||
|
||||
@@ -325,6 +355,13 @@ async function ingestInbound(env: OmniInboundEnvelopeV1) {
|
||||
businessConnectionId,
|
||||
title: asString(n.chatTitle),
|
||||
});
|
||||
const inbox = await upsertContactInbox({
|
||||
teamId,
|
||||
contactId,
|
||||
channel: "TELEGRAM",
|
||||
sourceExternalId: externalChatId,
|
||||
title: asString(n.chatTitle),
|
||||
});
|
||||
const rawEnvelope = {
|
||||
version: env.version,
|
||||
source: "omni_chat.receiver",
|
||||
@@ -337,7 +374,7 @@ async function ingestInbound(env: OmniInboundEnvelopeV1) {
|
||||
normalized: {
|
||||
text,
|
||||
threadExternalId: externalChatId,
|
||||
contactExternalId,
|
||||
contactExternalId: externalContactId,
|
||||
businessConnectionId,
|
||||
},
|
||||
payloadNormalized: n,
|
||||
@@ -393,6 +430,7 @@ async function ingestInbound(env: OmniInboundEnvelopeV1) {
|
||||
await prisma.contactMessage.create({
|
||||
data: {
|
||||
contactId,
|
||||
contactInboxId: inbox.id,
|
||||
kind: "MESSAGE",
|
||||
direction,
|
||||
channel: "TELEGRAM",
|
||||
|
||||
Reference in New Issue
Block a user