Files
libredesk/internal/conversation/queries.sql
Abhinav Raut 703f38fafc quote only messages the contact already saw in email history
Quoted history now leaves out CSAT emails and anything the contact was not part of. Earlier messages load only when the outgoing template or the reply uses PreviousMessages.
2026-10-20 00:00:37 +05:30

1377 lines
47 KiB
SQL

-- name: unsnooze-all
UPDATE conversations
SET snoozed_until = NULL, status_id = (SELECT id FROM conversation_statuses WHERE name = 'Open')
WHERE snoozed_until <= NOW()
AND status_id = (SELECT id FROM conversation_statuses WHERE name = 'Snoozed');
-- name: insert-conversation
-- $11 = rate limit window start (timestamptz), $12 = max conversations (0 = unlimited)
-- $13 = subject reference marker template (placeholder: {ref})
WITH
status_id AS (
SELECT id FROM conversation_statuses WHERE name = $2
),
reference_number AS (
SELECT generate_reference_number($7) AS reference_number
)
INSERT INTO conversations
(contact_id, status_id, inbox_id, last_message, last_message_at, subject, reference_number, meta, custom_attributes)
SELECT
$1,
(SELECT id FROM status_id),
$3,
$4,
$5,
CASE
WHEN $8 = TRUE THEN CONCAT($6::text, ' - ', REPLACE($13::text, '{ref}', (SELECT reference_number FROM reference_number)))
ELSE $6::text
END,
(SELECT reference_number FROM reference_number),
$9,
$10
WHERE $12::int = 0 OR (SELECT COUNT(*) FROM conversations WHERE contact_id = $1 AND created_at >= $11) < $12::int
RETURNING id, uuid;
-- name: get-conversations
-- $1 = viewing user ID for per-agent unread count
-- $2 = include mentioned message UUID (true for mentioned inbox, false otherwise)
SELECT
COUNT(*) OVER() as total,
conversations.id,
conversations.created_at,
conversations.updated_at,
conversations.uuid,
conversations.reference_number,
conversations.waiting_since,
users.created_at as "contact.created_at",
users.updated_at as "contact.updated_at",
users.first_name as "contact.first_name",
users.last_name as "contact.last_name",
users.email as "contact.email",
users.avatar_url as "contact.avatar_url",
inboxes.channel as inbox_channel,
inboxes.name as inbox_name,
conversations.sla_policy_id,
conversations.first_reply_at,
conversations.last_reply_at,
conversations.resolved_at,
conversations.last_resolved_at,
conversations.subject,
conversations.last_message,
conversations.last_message_at,
conversations.last_message_sender,
conversations.last_interaction,
conversations.last_interaction_at,
conversations.last_interaction_sender,
conversations.next_sla_deadline_at,
conversations.priority_id,
conversations.assigned_user_id,
conversations.assigned_team_id,
CASE WHEN assignee.id IS NULL THEN 'null'::json ELSE json_build_object(
'id', assignee.id,
'first_name', assignee.first_name,
'last_name', assignee.last_name,
'avatar_url', assignee.avatar_url,
'type', assignee.type,
'availability_status', assignee.availability_status
) END AS assignee,
(
SELECT CASE WHEN COUNT(*) > 9 THEN 10 ELSE COUNT(*) END
FROM (
SELECT 1 FROM conversation_messages
WHERE conversation_id = conversations.id
AND created_at > COALESCE(
(SELECT last_seen_at FROM conversation_last_seen
WHERE conversation_id = conversations.id AND user_id = $1),
'1970-01-01'::TIMESTAMPTZ
)
AND (meta IS NULL OR NOT COALESCE((meta->>'continuity_email')::boolean, false))
LIMIT 10
) t
) as unread_message_count,
conversation_statuses.name as status,
conversation_priorities.name as priority,
as_latest.first_response_deadline_at,
as_latest.resolution_deadline_at,
as_latest.id as applied_sla_id,
nxt_resp_event.deadline_at AS next_response_deadline_at,
nxt_resp_event.met_at as next_response_met_at,
CASE WHEN $2 = true THEN (
SELECT msg.uuid
FROM conversation_mentions cm2
JOIN conversation_messages msg ON msg.id = cm2.message_id
WHERE cm2.conversation_id = conversations.id
AND (cm2.mentioned_user_id = $1 OR EXISTS(
SELECT 1 FROM team_members tm2
WHERE tm2.team_id = cm2.mentioned_team_id AND tm2.user_id = $1
))
ORDER BY cm2.created_at DESC
LIMIT 1
) ELSE NULL END as mentioned_message_uuid
FROM conversations
JOIN users ON contact_id = users.id
LEFT JOIN users assignee ON conversations.assigned_user_id = assignee.id
AND assignee.deleted_at IS NULL AND (assignee.email != 'System' OR assignee.type = 'ai_assistant')
JOIN inboxes ON inbox_id = inboxes.id
LEFT JOIN conversation_statuses ON status_id = conversation_statuses.id
LEFT JOIN conversation_priorities ON priority_id = conversation_priorities.id
LEFT JOIN LATERAL (
SELECT id, first_response_deadline_at, resolution_deadline_at
FROM applied_slas
WHERE conversation_id = conversations.id
ORDER BY created_at DESC LIMIT 1
) as_latest ON true
LEFT JOIN LATERAL (
SELECT se.deadline_at, se.met_at
FROM sla_events se
WHERE se.applied_sla_id = as_latest.id
AND se.type = 'next_response'
ORDER BY se.created_at DESC
LIMIT 1
) nxt_resp_event ON true
WHERE 1=1 %s
-- name: get-conversation-list-item
SELECT
conversations.id,
conversations.created_at,
conversations.updated_at,
conversations.uuid,
conversations.reference_number,
conversations.waiting_since,
users.created_at as "contact.created_at",
users.updated_at as "contact.updated_at",
users.first_name as "contact.first_name",
users.last_name as "contact.last_name",
users.email as "contact.email",
users.avatar_url as "contact.avatar_url",
inboxes.channel as inbox_channel,
inboxes.name as inbox_name,
conversations.sla_policy_id,
conversations.first_reply_at,
conversations.last_reply_at,
conversations.resolved_at,
conversations.subject,
conversations.last_message,
conversations.last_message_at,
conversations.last_message_sender,
conversations.last_interaction,
conversations.last_interaction_at,
conversations.last_interaction_sender,
conversations.next_sla_deadline_at,
conversations.priority_id,
conversations.assigned_user_id,
conversations.assigned_team_id,
CASE WHEN assignee.id IS NULL THEN 'null'::json ELSE json_build_object(
'id', assignee.id,
'first_name', assignee.first_name,
'last_name', assignee.last_name,
'avatar_url', assignee.avatar_url,
'type', assignee.type,
'availability_status', assignee.availability_status
) END AS assignee,
conversation_statuses.name as status,
conversation_priorities.name as priority,
as_latest.first_response_deadline_at,
as_latest.resolution_deadline_at,
as_latest.id as applied_sla_id,
nxt_resp_event.deadline_at AS next_response_deadline_at,
nxt_resp_event.met_at as next_response_met_at
FROM conversations
JOIN users ON contact_id = users.id
LEFT JOIN users assignee ON conversations.assigned_user_id = assignee.id
AND assignee.deleted_at IS NULL AND (assignee.email != 'System' OR assignee.type = 'ai_assistant')
JOIN inboxes ON inbox_id = inboxes.id
LEFT JOIN conversation_statuses ON status_id = conversation_statuses.id
LEFT JOIN conversation_priorities ON priority_id = conversation_priorities.id
LEFT JOIN LATERAL (
SELECT id, first_response_deadline_at, resolution_deadline_at
FROM applied_slas
WHERE conversation_id = conversations.id
ORDER BY created_at DESC LIMIT 1
) as_latest ON true
LEFT JOIN LATERAL (
SELECT se.deadline_at, se.met_at
FROM sla_events se
WHERE se.applied_sla_id = as_latest.id
AND se.type = 'next_response'
ORDER BY se.created_at DESC
LIMIT 1
) nxt_resp_event ON true
WHERE conversations.uuid = $1::uuid;
-- name: get-conversation
SELECT
c.id,
c.created_at,
c.updated_at,
c.closed_at,
c.resolved_at,
c.last_resolved_at,
c.contact_last_seen_at,
c.inbox_id,
inb.name as inbox_name,
COALESCE(inb.from, '') as inbox_mail,
COALESCE(inb.config->>'reply_to', '') as inbox_reply_to,
COALESCE((SELECT jsonb_agg(jsonb_build_object('email', email, 'verification_status', verification_status, 'verified_at', verified_at) ORDER BY position, id) FROM inbox_email_addresses WHERE inbox_id = inb.id AND kind = 'alias'), '[]'::jsonb) AS inbox_aliases,
COALESCE(inb.channel::TEXT, '') as inbox_channel,
c.status_id,
c.priority_id,
p.name as priority,
s.name as status,
s.category as status_category,
c.uuid,
c.reference_number,
c.first_reply_at,
c.last_reply_at,
c.waiting_since,
c.snoozed_until,
c.assigned_user_id,
c.assigned_team_id,
CASE WHEN assignee.id IS NULL THEN 'null'::json ELSE json_build_object(
'id', assignee.id,
'first_name', assignee.first_name,
'last_name', assignee.last_name,
'avatar_url', assignee.avatar_url,
'type', assignee.type,
'availability_status', assignee.availability_status
) END AS assignee,
c.subject,
c.contact_id,
c.sla_policy_id,
c.next_sla_deadline_at,
c.meta,
sla.name as sla_policy_name,
c.last_message_at,
c.last_message_sender,
c.last_message,
c.last_interaction,
c.last_interaction_at,
c.last_interaction_sender,
c.custom_attributes,
(SELECT COALESCE(
(SELECT json_agg(t.name)
FROM tags t
INNER JOIN conversation_tags ct ON ct.tag_id = t.id
WHERE ct.conversation_id = c.id),
'[]'::json
)) AS tags,
ct.id as "contact.id",
ct.created_at as "contact.created_at",
ct.updated_at as "contact.updated_at",
ct.first_name as "contact.first_name",
ct.last_name as "contact.last_name",
ct.email as "contact.email",
ct.type as "contact.type",
ct.availability_status as "contact.availability_status",
ct.avatar_url as "contact.avatar_url",
ct.phone_number as "contact.phone_number",
ct.phone_number_country_code as "contact.phone_number_country_code",
ct.country as "contact.country",
ct.custom_attributes as "contact.custom_attributes",
ct.enabled as "contact.enabled",
ct.last_active_at as "contact.last_active_at",
ct.last_login_at as "contact.last_login_at",
ct.external_user_id as "contact.external_user_id",
(SELECT json_agg(json_build_object('channel', cci.channel, 'identifier', cci.identifier))
FROM contact_channel_identities cci WHERE cci.contact_id = ct.id) as "contact.channel_identities",
as_latest.first_response_deadline_at,
as_latest.resolution_deadline_at,
as_latest.id as applied_sla_id,
nxt_resp_event.deadline_at AS next_response_deadline_at,
nxt_resp_event.met_at as next_response_met_at,
c.last_continuity_email_sent_at,
c.last_inbound_at,
(SELECT MAX(c2.last_inbound_at) FROM conversations c2 WHERE c2.contact_id = c.contact_id AND c2.inbox_id = c.inbox_id) AS contact_last_inbound_at,
csat.rating as csat_rating,
csat.feedback as csat_feedback,
csat.response_timestamp as csat_responded_at
FROM conversations c
JOIN users ct ON c.contact_id = ct.id
LEFT JOIN users assignee ON c.assigned_user_id = assignee.id
AND assignee.deleted_at IS NULL AND (assignee.email != 'System' OR assignee.type = 'ai_assistant')
JOIN inboxes inb ON c.inbox_id = inb.id
LEFT JOIN LATERAL (
SELECT rating, feedback, response_timestamp
FROM csat_responses
WHERE conversation_id = c.id
ORDER BY response_timestamp DESC NULLS LAST, created_at DESC
LIMIT 1
) csat ON true
LEFT JOIN sla_policies sla ON c.sla_policy_id = sla.id
LEFT JOIN teams at ON at.id = c.assigned_team_id
LEFT JOIN conversation_statuses s ON c.status_id = s.id
LEFT JOIN conversation_priorities p ON c.priority_id = p.id
LEFT JOIN LATERAL (
SELECT id, first_response_deadline_at, resolution_deadline_at
FROM applied_slas
WHERE conversation_id = c.id
ORDER BY created_at DESC LIMIT 1
) as_latest ON true
LEFT JOIN LATERAL (
SELECT se.deadline_at, se.met_at
FROM sla_events se
WHERE se.applied_sla_id = as_latest.id
AND se.type = 'next_response'
ORDER BY se.created_at DESC
LIMIT 1
) nxt_resp_event ON true
WHERE
($1 > 0 AND c.id = $1)
OR
(NULLIF($2, '')::uuid IS NOT NULL AND c.uuid = NULLIF($2, '')::uuid)
OR
($3::TEXT != '' AND c.reference_number = $3::TEXT)
-- name: get-conversations-created-after
SELECT
c.id,
c.uuid
FROM conversations c
WHERE c.created_at > $1 AND c.id > $2
ORDER BY c.id
LIMIT $3;
-- name: get-contact-previous-conversations
SELECT
c.id,
c.created_at,
c.updated_at,
c.uuid,
c.subject,
u.first_name AS "contact.first_name",
u.last_name AS "contact.last_name",
u.avatar_url AS "contact.avatar_url",
c.last_message as last_message,
c.last_message_at as last_message_at
FROM users u
JOIN conversations c ON c.contact_id = u.id
WHERE c.contact_id = $1
AND $5
AND (
$6
OR ($7 AND c.assigned_user_id = $3)
OR ($8 AND c.assigned_team_id = ANY($4::int[]))
OR ($9 AND c.assigned_team_id = ANY($4::int[]) AND c.assigned_user_id IS NULL)
OR ($10 AND c.assigned_user_id IS NULL AND c.assigned_team_id IS NULL)
)
ORDER BY c.created_at DESC
LIMIT $2;
-- name: get-contact-conversations-for-ai
SELECT
c.id,
c.reference_number,
c.subject,
cs.name AS status,
c.created_at,
c.last_message_at,
c.assigned_user_id,
c.assigned_team_id
FROM conversations c
LEFT JOIN conversation_statuses cs ON c.status_id = cs.id
WHERE c.contact_id = $1
AND c.id != $2
ORDER BY c.created_at DESC
LIMIT 50;
-- name: get-conversations-by-contact-email-for-ai
SELECT
c.id,
c.reference_number,
c.subject,
cs.name AS status,
c.created_at,
c.last_message_at,
c.assigned_user_id,
c.assigned_team_id,
TRIM(CONCAT(u.first_name, ' ', COALESCE(u.last_name, ''))) AS contact_name
FROM conversations c
JOIN users u ON c.contact_id = u.id
LEFT JOIN conversation_statuses cs ON c.status_id = cs.id
WHERE LOWER(u.email) = LOWER($1)
AND u.type = 'contact'
AND u.deleted_at IS NULL
ORDER BY c.created_at DESC
LIMIT 50;
-- name: get-chat-conversation
SELECT
COALESCE((SELECT json_agg(json_build_object('name', media.filename, 'content_type', media.content_type, 'uuid', media.uuid, 'size', media.size, 'content_id', media.content_id, 'disposition', media.disposition))
FROM media WHERE media.model_type = 'messages' AND media.model_id = (
SELECT m.id FROM conversation_messages m
WHERE m.conversation_id = c.id AND m.private = FALSE AND m.type IN ('incoming', 'outgoing')
ORDER BY m.created_at DESC, m.id DESC LIMIT 1
)), '[]'::json) AS "last_message.attachments",
COALESCE(c.contact_last_seen_at, c.created_at) AS contact_last_seen_at,
c.created_at,
c.uuid,
cs.name as status,
COALESCE(c.last_interaction, '') as "last_message.content",
c.last_interaction_at as "last_message.created_at",
COALESCE(lis.id, 0) AS "last_message.author.id",
COALESCE(lis.first_name, '') AS "last_message.author.first_name",
COALESCE(lis.last_name, '') AS "last_message.author.last_name",
COALESCE(lis.avatar_url, '') AS "last_message.author.avatar_url",
COALESCE(c.last_interaction_sender::TEXT, '') AS "last_message.author.type",
(SELECT CASE WHEN COUNT(*) > 9 THEN 10 ELSE COUNT(*) END
FROM (
SELECT 1 FROM conversation_messages unread
WHERE unread.conversation_id = c.id
AND unread.created_at > COALESCE(c.contact_last_seen_at, c.created_at)
AND unread.type = 'outgoing'
AND unread.private = false
LIMIT 10
) t) AS unread_message_count,
COALESCE(au.availability_status::TEXT, '') as "assignee.availability_status",
au.avatar_url as "assignee.avatar_url",
COALESCE(au.first_name, '') as "assignee.first_name",
COALESCE(au.id, 0) as "assignee.id",
COALESCE(au.last_name, '') as "assignee.last_name",
COALESCE(au.type::TEXT, '') as "assignee.type",
COALESCE(aa.expectation, '') as "assignee.expectation"
FROM conversations c
INNER JOIN inboxes inb on c.inbox_id = inb.id
LEFT JOIN conversation_statuses cs ON c.status_id = cs.id
LEFT JOIN users au ON c.assigned_user_id = au.id
LEFT JOIN ai_assistants aa ON aa.user_id = au.id
LEFT JOIN users lis ON c.last_interaction_sender_id = lis.id
WHERE c.uuid = $1
AND inb.deleted_at IS NULL;
-- name: get-contact-unread-preview-messages
SELECT
m.id,
m.created_at,
m.status,
m.type,
m.content,
m.text_content,
m.content_type,
m.conversation_id,
m.uuid,
m.private,
m.sender_id,
m.sender_type,
m.meta,
c.uuid AS conversation_uuid,
u.id AS "author.id",
u.first_name AS "author.first_name",
u.last_name AS "author.last_name",
u.email AS "author.email",
u.avatar_url AS "author.avatar_url",
u.availability_status AS "author.availability_status",
u.type AS "author.type",
u.last_active_at AS "author.last_active_at",
COALESCE(
(SELECT json_agg(
json_build_object(
'name', filename,
'content_type', content_type,
'uuid', uuid,
'size', size,
'content_id', content_id,
'disposition', disposition
) ORDER BY filename
) FROM media
WHERE model_type = 'messages' AND model_id = m.id),
'[]'::json) AS attachments
FROM conversation_messages m
JOIN conversations c ON c.id = m.conversation_id
JOIN inboxes inb ON inb.id = c.inbox_id
JOIN users u ON u.id = m.sender_id
WHERE c.contact_id = $1
AND c.inbox_id = $2
AND inb.deleted_at IS NULL
AND m.created_at > COALESCE(c.contact_last_seen_at, c.created_at)
AND m.type = 'outgoing'
AND m.private = false
AND u.type IN ('agent', 'ai_assistant')
AND (m.meta IS NULL OR NOT COALESCE((m.meta->>'continuity_email')::boolean, false))
ORDER BY m.created_at DESC, m.id DESC
LIMIT $3;
-- name: get-contact-chat-conversations
SELECT
COALESCE((SELECT json_agg(json_build_object('name', media.filename, 'content_type', media.content_type, 'uuid', media.uuid, 'size', media.size, 'content_id', media.content_id, 'disposition', media.disposition))
FROM media WHERE media.model_type = 'messages' AND media.model_id = (
SELECT m.id FROM conversation_messages m
WHERE m.conversation_id = c.id AND m.private = FALSE AND m.type IN ('incoming', 'outgoing')
ORDER BY m.created_at DESC, m.id DESC LIMIT 1
)), '[]'::json) AS "last_message.attachments",
COALESCE(c.contact_last_seen_at, c.created_at) AS contact_last_seen_at,
c.created_at,
c.uuid,
cs.name as status,
COALESCE(c.last_interaction, '') as "last_message.content",
c.last_interaction_at as "last_message.created_at",
COALESCE(lis.id, 0) AS "last_message.author.id",
COALESCE(lis.first_name, '') AS "last_message.author.first_name",
COALESCE(lis.last_name, '') AS "last_message.author.last_name",
COALESCE(lis.avatar_url, '') AS "last_message.author.avatar_url",
COALESCE(c.last_interaction_sender::TEXT, '') AS "last_message.author.type",
(SELECT CASE WHEN COUNT(*) > 9 THEN 10 ELSE COUNT(*) END
FROM (
SELECT 1 FROM conversation_messages unread
WHERE unread.conversation_id = c.id
AND unread.created_at > COALESCE(c.contact_last_seen_at, c.created_at)
AND unread.type = 'outgoing'
AND unread.private = false
LIMIT 10
) t) AS unread_message_count,
COALESCE(au.availability_status::TEXT, '') as "assignee.availability_status",
au.avatar_url as "assignee.avatar_url",
COALESCE(au.first_name, '') as "assignee.first_name",
COALESCE(au.id, 0) as "assignee.id",
COALESCE(au.last_name, '') as "assignee.last_name",
COALESCE(au.type::TEXT, '') as "assignee.type"
FROM conversations c
INNER JOIN inboxes inb ON c.inbox_id = inb.id
INNER JOIN users con ON c.contact_id = con.id
LEFT JOIN conversation_statuses cs ON c.status_id = cs.id
LEFT JOIN users au ON c.assigned_user_id = au.id
LEFT JOIN users lis ON c.last_interaction_sender_id = lis.id
WHERE c.contact_id = $1 AND c.inbox_id = $2
AND inb.deleted_at IS NULL
AND con.deleted_at IS NULL
ORDER BY c.created_at DESC
LIMIT 200;
-- name: get-conversation-uuid
SELECT uuid from conversations where id = $1;
-- name: get-conversation-inbox-contact
SELECT inbox_id, contact_id FROM conversations WHERE uuid = $1;
-- name: update-conversation-assigned-user
UPDATE conversations
SET assigned_user_id = $2,
updated_at = NOW()
WHERE uuid = $1;
-- name: claim-unassigned-conversation
UPDATE conversations
SET assigned_user_id = $2,
updated_at = NOW()
WHERE uuid = $1 AND assigned_user_id IS NULL AND assigned_team_id = $3;
-- name: update-conversation-contact-last-seen
UPDATE conversations
SET contact_last_seen_at = NOW(),
updated_at = NOW()
WHERE uuid = $1
RETURNING contact_last_seen_at;
-- name: update-conversation-assigned-team
UPDATE conversations
SET assigned_team_id = $2,
updated_at = NOW()
WHERE uuid = $1;
-- name: update-conversation-status
WITH new_status AS (
SELECT id, category FROM conversation_statuses WHERE name = $2
)
UPDATE conversations
SET status_id = (SELECT id FROM new_status),
resolved_at = COALESCE(resolved_at, CASE WHEN (SELECT category FROM new_status) = 'resolved' THEN NOW() END),
last_resolved_at = CASE WHEN (SELECT category FROM new_status) = 'resolved' THEN NOW() ELSE last_resolved_at END,
closed_at = COALESCE(closed_at, CASE WHEN $2 = 'Closed' THEN NOW() END),
snoozed_until = CASE WHEN $2 = 'Snoozed' THEN $3::timestamptz ELSE NULL END,
updated_at = NOW()
WHERE uuid = $1;
-- name: get-user-active-conversations-count
SELECT COUNT(*) FROM conversations WHERE status_id IN (SELECT id FROM conversation_statuses WHERE category = 'open') AND assigned_user_id = $1;
-- name: get-sidebar-standard-counts
SELECT
COUNT(*) FILTER (WHERE conversations.assigned_user_id = $1) AS assigned,
COUNT(*) FILTER (WHERE conversations.assigned_user_id IS NULL AND conversations.assigned_team_id IS NULL) AS unassigned,
COUNT(*) FILTER (WHERE EXISTS (
SELECT 1 FROM conversation_mentions cm
WHERE cm.conversation_id = conversations.id
AND (cm.mentioned_user_id = $1 OR EXISTS (
SELECT 1 FROM team_members tm
WHERE tm.team_id = cm.mentioned_team_id AND tm.user_id = $1
))
)) AS mentioned,
COUNT(*) AS "all"
FROM conversations
WHERE conversations.status_id IN (SELECT id FROM conversation_statuses WHERE category = 'open');
-- name: get-conversations-count-base
-- The list-type WHERE clause is appended at %s; view filters are added by BuildFilterQuery.
SELECT 1
FROM conversations
JOIN users ON contact_id = users.id
JOIN inboxes ON inbox_id = inboxes.id
LEFT JOIN conversation_statuses ON status_id = conversation_statuses.id
WHERE TRUE
%s
-- name: update-conversation-priority
UPDATE conversations
SET priority_id = (SELECT id FROM conversation_priorities WHERE name = $2),
updated_at = NOW()
WHERE uuid = $1;
-- name: upsert-user-last-seen
INSERT INTO conversation_last_seen (user_id, conversation_id, last_seen_at, updated_at)
SELECT $1, c.id, COALESCE(m.created_at, NOW()), clock_timestamp()
FROM conversations c
LEFT JOIN conversation_messages m ON m.conversation_id = c.id AND m.uuid = NULLIF($3::text, '')::uuid
WHERE c.uuid = $2 AND ($3::text = '' OR m.id IS NOT NULL)
ON CONFLICT (conversation_id, user_id)
DO UPDATE SET
last_seen_at = GREATEST(conversation_last_seen.last_seen_at, EXCLUDED.last_seen_at),
updated_at = GREATEST(conversation_last_seen.updated_at + INTERVAL '1 microsecond', clock_timestamp())
RETURNING last_seen_at, (EXTRACT(EPOCH FROM updated_at) * 1000000)::bigint AS read_version;
-- name: update-conversation-last-message
-- $1=id, $2=uuid, $3=content, $4=sender_type, $5=timestamp, $6=message_type, $7=private, $8=sender_id
UPDATE conversations SET
last_message = $3,
last_message_sender = $4,
last_message_sender_id = $8,
last_message_at = $5,
last_interaction = CASE WHEN $6 != 'activity' AND $7 = false THEN $3 ELSE last_interaction END,
last_interaction_sender = CASE WHEN $6 != 'activity' AND $7 = false THEN $4 ELSE last_interaction_sender END,
last_interaction_sender_id = CASE WHEN $6 != 'activity' AND $7 = false THEN $8 ELSE last_interaction_sender_id END,
last_interaction_at = CASE WHEN $6 != 'activity' AND $7 = false THEN $5 ELSE last_interaction_at END,
updated_at = NOW()
WHERE CASE
WHEN $1 > 0 THEN id = $1
ELSE uuid = $2
END
-- name: get-conversation-participants
SELECT users.id as id, first_name, last_name, avatar_url
FROM conversation_participants
INNER JOIN users ON users.id = conversation_participants.user_id
WHERE conversation_id =
(
SELECT id FROM conversations WHERE uuid = $1
);
-- name: get-conversation-participant-agents
SELECT users.id, users.first_name, users.last_name, users.email
FROM conversation_participants
INNER JOIN users ON users.id = conversation_participants.user_id
WHERE conversation_participants.conversation_id = (SELECT id FROM conversations WHERE uuid = $1)
AND users.type = 'agent'
AND EXISTS (
SELECT 1 FROM conversation_messages m
WHERE m.conversation_id = conversation_participants.conversation_id
AND m.sender_id = users.id
AND m.sender_type = 'agent'
AND m.type = 'outgoing'
AND NOT m.private
)
AND users.email != 'System'
AND users.enabled
AND users.deleted_at IS NULL;
-- name: insert-conversation-participant
INSERT INTO conversation_participants
(user_id, conversation_id)
VALUES($1, (SELECT id FROM conversations WHERE uuid = $2))
ON CONFLICT (conversation_id, user_id) DO NOTHING;
-- name: get-unassigned-conversations
SELECT
c.created_at,
c.updated_at,
c.uuid,
c.assigned_team_id,
inb.channel as inbox_channel,
inb.name as inbox_name
FROM conversations c
JOIN inboxes inb ON c.inbox_id = inb.id
WHERE assigned_user_id IS NULL AND assigned_team_id IS NOT NULL
ORDER BY c.created_at ASC;
-- name: add-conversation-tags
-- Insert new tags
INSERT INTO conversation_tags (conversation_id, tag_id)
SELECT c.id, t.id
FROM conversations c, tags t
WHERE t.name = ANY($2::text[]) AND c.uuid = $1
ON CONFLICT (conversation_id, tag_id) DO UPDATE SET tag_id = EXCLUDED.tag_id;
-- name: set-conversation-tags
WITH conversation_id AS (
SELECT id FROM conversations WHERE uuid = $1
),
-- Insert new tags
inserted AS (
INSERT INTO conversation_tags (conversation_id, tag_id)
SELECT conversation_id.id, t.id
FROM conversation_id, tags t
WHERE t.name = ANY($2::text[])
ON CONFLICT (conversation_id, tag_id) DO UPDATE SET tag_id = EXCLUDED.tag_id
)
-- Delete tags that are not in the new list
DELETE FROM conversation_tags
WHERE conversation_id = (SELECT id FROM conversation_id)
AND tag_id NOT IN (
SELECT id FROM tags WHERE name = ANY($2::text[])
);
-- name: remove-conversation-tags
-- Delete tags that are not in the new list
DELETE FROM conversation_tags
WHERE conversation_id = (SELECT id FROM conversations WHERE uuid = $1)
AND tag_id IN (
SELECT id FROM tags WHERE name = ANY($2::text[])
);
-- name: get-conversation-tags
SELECT t.name
FROM conversation_tags ct
JOIN tags t ON ct.tag_id = t.id
WHERE ct.conversation_id = (SELECT id FROM conversations WHERE uuid = $1);
-- name: get-conversation-uuid-from-message-uuid
SELECT c.uuid AS conversation_uuid
FROM conversation_messages m
JOIN conversations c ON m.conversation_id = c.id
WHERE m.uuid = $1;
-- name: unassign-open-conversations
UPDATE conversations
SET assigned_user_id = NULL,
updated_at = NOW()
WHERE assigned_user_id = $1 AND status_id IN (SELECT id FROM conversation_statuses WHERE category != 'resolved');
-- name: update-conversation-custom-attributes
UPDATE conversations
SET custom_attributes = $2,
updated_at = NOW()
WHERE uuid = $1;
-- name: start-conversation-waiting-since
UPDATE conversations
SET waiting_since = $2,
updated_at = NOW()
WHERE uuid = $1 AND waiting_since IS NULL;
-- name: update-conversation-reply-timestamps
WITH old AS (
SELECT first_reply_at IS NULL AS is_first FROM conversations WHERE id = $1
)
UPDATE conversations SET
first_reply_at = COALESCE(conversations.first_reply_at, $2),
last_reply_at = $2,
waiting_since = NULL,
updated_at = NOW()
FROM old WHERE conversations.id = $1
RETURNING old.is_first AS is_first_reply;
-- name: remove-conversation-assignee
UPDATE conversations
SET
assigned_user_id = CASE WHEN $2 = 'user' THEN NULL ELSE assigned_user_id END,
assigned_team_id = CASE WHEN $2 = 'team' THEN NULL ELSE assigned_team_id END,
updated_at = NOW()
WHERE uuid = $1;
-- name: re-open-conversation
-- Open conversation if it is not already open and unset the assigned user if they are away and reassigning.
UPDATE conversations
SET
status_id = (SELECT id FROM conversation_statuses WHERE name = 'Open'),
snoozed_until = NULL,
updated_at = NOW(),
assigned_user_id = CASE
WHEN EXISTS (
SELECT 1 FROM users
WHERE users.id = conversations.assigned_user_id
AND users.availability_status = 'away_and_reassigning'
) THEN NULL
ELSE assigned_user_id
END
WHERE
uuid = $1
AND status_id IN (
SELECT id FROM conversation_statuses WHERE name NOT IN ('Open')
)
RETURNING id;
-- name: get-conversation-by-message-id
SELECT
c.id,
c.uuid,
c.assigned_team_id,
c.assigned_user_id
FROM conversation_messages m
JOIN conversations c ON m.conversation_id = c.id
WHERE m.id = $1;
-- name: delete-conversation
DELETE FROM conversations WHERE uuid = $1;
-- MESSAGE queries.
-- name: delete-message
DELETE FROM conversation_messages WHERE CASE
WHEN $1 > 0 THEN id = $1
ELSE uuid = $2
END;
-- name: delete-private-message
-- $1 = message uuid, $2 = conversation uuid, $3 = deleted placeholder text, $4 = sender id, 0 to skip the sender check.
WITH deleted AS (
UPDATE conversation_messages
SET content = $3, text_content = $3, updated_at = NOW(),
meta = COALESCE(meta, '{}'::jsonb) || jsonb_build_object('deleted_at', NOW())
WHERE uuid = $1
AND private = true
AND meta->>'deleted_at' IS NULL
AND ($4 = 0 OR sender_id = $4)
AND conversation_id = (SELECT id FROM conversations WHERE uuid = $2)
RETURNING id, conversation_id, created_at
),
media_unlink AS (
UPDATE media SET model_id = 0
FROM deleted d
WHERE media.model_type = 'messages' AND media.model_id = d.id
),
preview AS (
UPDATE conversations c
SET last_message = $3, updated_at = NOW()
FROM deleted d
WHERE c.id = d.conversation_id
AND NOT EXISTS (
SELECT 1 FROM conversation_messages m2
WHERE m2.conversation_id = d.conversation_id
AND m2.created_at > d.created_at
)
RETURNING c.id
)
SELECT EXISTS (SELECT 1 FROM preview) AS preview_updated
FROM deleted d;
-- name: get-message-source-ids
SELECT
source_id
FROM conversation_messages
WHERE conversation_id = $1
AND type in ('incoming', 'outgoing') and private = false
and source_id > ''
ORDER BY id DESC
LIMIT $2;
-- name: get-previous-messages
SELECT
m.created_at,
m.type = 'incoming' AS is_incoming,
COALESCE(m.content, '') AS content,
COALESCE(m.text_content, '') AS text_content,
COALESCE(m.content_type::text, '') AS content_type,
u.first_name,
COALESCE(u.last_name, '') AS last_name,
COALESCE(u.email, '') AS email
FROM conversation_messages m
JOIN conversations c ON c.id = m.conversation_id
JOIN users u ON u.id = m.sender_id
JOIN users ct ON ct.id = c.contact_id
WHERE m.conversation_id = $1
AND m.id < $2
AND m.private = false
AND m.status != 'failed'
AND NOT COALESCE((m.meta->>'continuity_email')::boolean, false)
AND NOT COALESCE((m.meta->>'is_csat')::boolean, false)
AND (
(m.type = 'incoming' AND m.sender_id = c.contact_id)
OR (m.type = 'outgoing' AND EXISTS (
SELECT 1
FROM jsonb_array_elements_text(COALESCE(m.meta->'to', '[]'::jsonb) || COALESCE(m.meta->'cc', '[]'::jsonb)) AS r(addr)
WHERE lower(r.addr) = lower(ct.email)
))
)
ORDER BY m.id DESC
LIMIT $3;
-- name: get-email-sender-message
SELECT m.type, m.meta
FROM conversation_messages m
JOIN conversations c ON c.id = m.conversation_id
WHERE c.uuid = $1 AND m.private = false AND m.type IN ('incoming', 'outgoing')
ORDER BY (m.type = 'outgoing') DESC, m.id DESC
LIMIT 1;
-- name: get-outgoing-pending-messages
SELECT
m.id,
m.created_at,
m.updated_at,
m.status,
m.type,
m.content,
m.text_content,
m.sender_type,
m.content_type,
m.conversation_id,
m.uuid,
m.private,
m.sender_type,
m.sender_id,
m.meta,
c.uuid as conversation_uuid,
m.content_type,
m.source_id,
m.meta,
ARRAY(SELECT jsonb_array_elements_text(m.meta->'cc')) AS cc,
ARRAY(SELECT jsonb_array_elements_text(m.meta->'bcc')) AS bcc,
ARRAY(SELECT jsonb_array_elements_text(m.meta->'to')) AS to,
c.inbox_id,
c.uuid as conversation_uuid,
c.subject,
c.contact_id as message_receiver_id,
c.subject,
c.reference_number
FROM conversation_messages m
INNER JOIN conversations c ON c.id = m.conversation_id
WHERE m.status = 'pending' AND m.type = 'outgoing' AND m.private = false
AND NOT (m.conversation_id = ANY($1::INT[]))
AND NOT EXISTS (
SELECT 1
FROM conversation_messages earlier
WHERE earlier.conversation_id = m.conversation_id
AND earlier.status = 'pending'
AND earlier.type = 'outgoing'
AND earlier.private = false
AND earlier.id < m.id
)
ORDER BY m.id
-- name: get-message
SELECT
m.id,
m.created_at,
m.updated_at,
m.status,
m.type,
m.content,
m.text_content,
m.content_type,
m.conversation_id,
m.uuid,
m.private,
m.sender_type,
m.sender_id,
m.meta,
c.uuid as conversation_uuid,
u.id AS "author.id",
u.first_name AS "author.first_name",
u.last_name AS "author.last_name",
u.email AS "author.email",
u.avatar_url AS "author.avatar_url",
u.availability_status AS "author.availability_status",
u.type AS "author.type",
u.last_active_at AS "author.last_active_at",
COALESCE(
json_agg(
json_build_object(
'name', media.filename,
'content_type', media.content_type,
'uuid', media.uuid,
'size', media.size,
'content_id', media.content_id,
'disposition', media.disposition
) ORDER BY media.filename
) FILTER (WHERE media.id IS NOT NULL),
'[]'::json
) AS attachments
FROM conversation_messages m
INNER JOIN conversations c ON c.id = m.conversation_id
JOIN users u ON m.sender_id = u.id
LEFT JOIN media ON media.model_type = 'messages' AND media.model_id = m.id
WHERE m.uuid = $1
GROUP BY
m.id, m.created_at, m.updated_at, m.status, m.type, m.content, m.uuid, m.private, m.sender_type, c.uuid,
u.id, u.first_name, u.last_name, u.email, u.avatar_url, u.availability_status, u.type, u.last_active_at
ORDER BY m.created_at;
-- name: get-messages
SELECT
COUNT(*) OVER() AS total,
m.id,
m.created_at,
m.updated_at,
m.status,
m.type,
m.content,
m.text_content,
m.content_type,
m.conversation_id,
m.uuid,
m.private,
m.sender_id,
m.sender_type,
m.meta,
$1::uuid AS conversation_uuid,
u.id AS "author.id",
u.first_name AS "author.first_name",
u.last_name AS "author.last_name",
u.email AS "author.email",
u.avatar_url AS "author.avatar_url",
u.availability_status AS "author.availability_status",
u.type AS "author.type",
u.last_active_at AS "author.last_active_at",
COALESCE(
(SELECT json_agg(
json_build_object(
'name', filename,
'content_type', content_type,
'uuid', uuid,
'size', size,
'content_id', content_id,
'disposition', disposition
) ORDER BY filename
) FROM media
WHERE model_type = 'messages' AND model_id = m.id),
'[]'::json) AS attachments
FROM conversation_messages m
JOIN users u ON m.sender_id = u.id
WHERE m.conversation_id = (
SELECT id FROM conversations WHERE uuid = $1 LIMIT 1
)
AND ($2::boolean IS NULL OR m.private = $2)
AND ($3::text[] IS NULL OR m.type::text = ANY($3))
AND (m.meta IS NULL OR NOT COALESCE((m.meta->>'continuity_email')::boolean, false))
ORDER BY m.created_at DESC, m.id DESC %s
-- name: insert-message
WITH conversation_id AS (
SELECT id
FROM conversations
WHERE CASE
WHEN $3 > 0 THEN id = $3
ELSE uuid = $4
END
),
inserted_msg AS (
INSERT INTO conversation_messages (
"type", status, conversation_id, "content",
text_content, sender_id, sender_type, private,
content_type, source_id, meta
)
VALUES (
$1, $2, (SELECT id FROM conversation_id),
$5, $6, $7, $8, $9, $10, $11, $12
)
RETURNING *
)
SELECT * FROM inserted_msg;
-- name: message-exists-by-source-id
SELECT conversation_id
FROM conversation_messages
WHERE source_id = ANY($1::text []);
-- name: update-message-status
UPDATE conversation_messages SET status = $1::message_status, updated_at = NOW()
WHERE uuid = $2 AND NOT ($1 = 'sent' AND status = 'failed');
-- name: mark-message-pending-for-retry
-- Keeping the old wamid or provider_status would let a late webhook re-fail the retried row.
UPDATE conversation_messages m
SET status = 'pending',
source_id = CASE WHEN inb.channel = 'whatsapp' THEN NULL ELSE m.source_id END,
meta = COALESCE(m.meta, '{}'::jsonb)
- 'provider_status' - 'provider_status_updated_at' - 'provider_sent_at'
- 'provider_delivered_at' - 'provider_read_at' - 'provider_failed_at'
- 'provider_failure_reason',
updated_at = NOW()
FROM conversations c
JOIN inboxes inb ON inb.id = c.inbox_id
WHERE m.uuid = $1 AND c.id = m.conversation_id AND m.status IN ('failed', 'sent');
-- name: update-message-source-id
UPDATE conversation_messages SET source_id = $1 WHERE id = $2;
-- name: update-message-source-id-by-uuid
UPDATE conversation_messages SET source_id = $2, updated_at = NOW() WHERE uuid = $1;
-- name: merge-message-meta-by-uuid
UPDATE conversation_messages m
SET meta = COALESCE(m.meta, '{}'::jsonb) || $2::jsonb,
updated_at = NOW()
FROM conversations c
WHERE m.uuid = $1
AND c.id = m.conversation_id
RETURNING m.uuid, c.uuid AS conversation_uuid, m.meta;
-- name: apply-whatsapp-message-status
-- Meta guard is monotonic (rank order) and sticky on failure. The enum status only guards against un-failing a failed message.
WITH ranks(status, rank) AS (
VALUES ('sent', 1), ('delivered', 2), ('read', 3), ('failed', 4)
)
UPDATE conversation_messages m
SET status = CASE WHEN m.status != 'failed' THEN $2::message_status ELSE m.status END,
meta = CASE
WHEN COALESCE(m.meta->>'provider_status', '') != 'failed'
AND COALESCE(
(SELECT rank FROM ranks WHERE status = ($3::jsonb)->>'provider_status'),
0
) >= COALESCE(
(SELECT rank FROM ranks WHERE status = m.meta->>'provider_status'),
0
)
THEN COALESCE(m.meta, '{}'::jsonb) || $3::jsonb
ELSE m.meta
END,
updated_at = NOW()
FROM conversations c
WHERE m.source_id = $1
AND c.id = m.conversation_id
RETURNING m.uuid, c.uuid AS conversation_uuid, m.status, m.meta;
-- name: get-whatsapp-read-receipt-target
SELECT cm.source_id, c.inbox_id
FROM conversation_messages cm
JOIN conversations c ON c.id = cm.conversation_id
JOIN inboxes i ON i.id = c.inbox_id
WHERE c.uuid = $1
AND i.channel = 'whatsapp'
AND ($3::text = '' OR cm.created_at <= (SELECT created_at FROM conversation_messages WHERE conversation_id = c.id AND uuid = NULLIF($3::text, '')::uuid))
AND cm.type = 'incoming'
AND COALESCE(cm.source_id, '') != ''
AND cm.created_at > COALESCE(
(SELECT last_seen_at FROM conversation_last_seen ls
WHERE ls.conversation_id = c.id AND ls.user_id = $2),
'epoch'::timestamptz)
ORDER BY cm.created_at DESC
LIMIT 1;
-- name: update-conversation-last-inbound-at
UPDATE conversations
SET last_inbound_at = GREATEST(last_inbound_at, $2),
updated_at = CASE WHEN last_inbound_at IS NULL OR last_inbound_at < $2 THEN NOW() ELSE updated_at END
WHERE id = $1
RETURNING contact_id, inbox_id;
-- name: get-contact-window-inbound-at
SELECT MAX(last_inbound_at) FROM conversations WHERE contact_id = $1 AND inbox_id = $2;
-- name: get-latest-open-conversation-by-contact-inbox
SELECT id, uuid
FROM conversations
WHERE contact_id = $1
AND inbox_id = $2
AND status_id IN (SELECT id FROM conversation_statuses WHERE category != 'resolved')
ORDER BY last_interaction_at DESC NULLS LAST, created_at DESC
LIMIT 1;
-- name: get-latest-reopenable-conversation-by-contact-inbox
SELECT id, uuid
FROM conversations
WHERE contact_id = $1
AND inbox_id = $2
AND status_id IN (SELECT id FROM conversation_statuses WHERE category = 'resolved')
AND last_resolved_at IS NOT NULL
AND last_resolved_at >= NOW() - make_interval(hours => $3)
ORDER BY last_resolved_at DESC
LIMIT 1;
-- name: clear-message-handoff-form-pending
UPDATE conversation_messages
SET meta = meta || '{"handoff_form_pending": false}'::jsonb, updated_at = NOW()
WHERE uuid = $1 AND COALESCE((meta->>'handoff_form_pending')::boolean, false);
-- name: get-offline-livechat-conversations
SELECT
c.id,
c.uuid,
c.contact_id,
c.inbox_id,
c.contact_last_seen_at,
c.last_continuity_email_sent_at,
i.linked_email_inbox_id,
u.email as contact_email,
u.first_name as contact_first_name,
u.last_name as contact_last_name,
c.reference_number,
c.meta->>'continuity_email_subject' as continuity_email_subject
FROM conversations c
JOIN users u ON u.id = c.contact_id
JOIN inboxes i ON i.id = c.inbox_id
WHERE i.channel = 'livechat'
AND i.enabled = TRUE
AND i.linked_email_inbox_id IS NOT NULL
AND c.contact_last_seen_at IS NOT NULL
AND c.contact_last_seen_at < NOW() - MAKE_INTERVAL(mins => $1)
AND EXISTS (
SELECT 1 FROM conversation_messages cm
WHERE cm.conversation_id = c.id
AND cm.created_at > c.contact_last_seen_at
AND cm.type = 'outgoing'
AND cm.private = false
AND (cm.meta IS NULL OR NOT COALESCE((cm.meta->>'continuity_email')::boolean, false))
AND (cm.meta IS NULL OR NOT COALESCE((cm.meta->>'continuity_emailed')::boolean, false))
)
AND u.email > ''
AND (c.last_continuity_email_sent_at IS NULL
OR c.last_continuity_email_sent_at < NOW() - MAKE_INTERVAL(mins => $2))
AND c.inbox_id = $3;
-- name: get-unread-messages
SELECT
m.id,
m.created_at,
m.updated_at,
m.status,
m.type,
m.content,
m.text_content,
m.uuid,
m.private,
m.sender_id,
m.sender_type,
m.meta,
u.first_name as "sender.first_name",
u.last_name as "sender.last_name",
u.type as "sender.type",
COALESCE((SELECT string_agg(md.filename, ',') FROM media md WHERE md.model_id = m.id AND md.model_type = 'messages'), '') AS attachment_names
FROM conversation_messages m
LEFT JOIN users u ON u.id = m.sender_id
WHERE m.conversation_id = $1
AND m.created_at > $2
AND m.type = 'outgoing'
AND m.private = false
AND (m.meta IS NULL OR NOT COALESCE((m.meta->>'continuity_email')::boolean, false))
AND (m.meta IS NULL OR NOT COALESCE((m.meta->>'continuity_emailed')::boolean, false))
ORDER BY m.created_at ASC
LIMIT $3;
-- name: mark-messages-continuity-emailed
UPDATE conversation_messages
SET meta = COALESCE(meta, '{}'::jsonb) || '{"continuity_emailed": true}'::jsonb
WHERE id = ANY($1::int[]);
-- name: update-continuity-email-tracking
UPDATE conversations
SET last_continuity_email_sent_at = NOW(),
meta = jsonb_set(COALESCE(meta, '{}'::jsonb), '{continuity_email_subject}', to_jsonb($2::text))
WHERE id = $1;
-- name: upsert-conversation-draft
INSERT INTO conversation_drafts (conversation_id, user_id, type, content, meta, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (conversation_id, user_id, type)
DO UPDATE SET content = EXCLUDED.content, meta = EXCLUDED.meta, updated_at = NOW()
RETURNING *;
-- name: get-all-user-drafts
SELECT cd.id, cd.conversation_id, cd.user_id, cd.type, cd.content, cd.meta, cd.created_at, cd.updated_at, c.uuid as conversation_uuid
FROM conversation_drafts cd
INNER JOIN conversations c ON cd.conversation_id = c.id
WHERE cd.user_id = $1
ORDER BY cd.updated_at DESC;
-- name: delete-conversation-draft
DELETE FROM conversation_drafts
WHERE conversation_id IN (
SELECT id FROM conversations
WHERE ($1 > 0 AND id = $1) OR (NULLIF($2, '')::uuid IS NOT NULL AND uuid = NULLIF($2, '')::uuid)
) AND user_id = $3
AND ($4::text = '' OR type = $4::text);
-- name: delete-stale-drafts
DELETE FROM conversation_drafts
WHERE created_at < $1;
-- name: insert-mention
INSERT INTO conversation_mentions (conversation_id, message_id, mentioned_user_id, mentioned_team_id, mentioned_by_user_id)
VALUES ($1, $2, $3, $4, $5);
-- name: mark-conversation-unread
WITH target AS (
SELECT id FROM conversations WHERE uuid = $2
),
last_msg AS (
SELECT created_at - INTERVAL '1 second' AS ts
FROM conversation_messages
WHERE conversation_id = (SELECT id FROM target)
AND (meta IS NULL OR NOT COALESCE((meta->>'continuity_email')::boolean, false))
ORDER BY created_at DESC LIMIT 1
),
read_replies AS (
UPDATE user_notifications n SET is_read = true, updated_at = NOW()
FROM conversation_messages m, conversation_last_seen ls
WHERE n.user_id = $1 AND n.conversation_id = (SELECT id FROM target) AND NOT n.is_read
AND n.notification_type = ANY($3::user_notification_type[])
AND m.id = n.message_id
AND ls.user_id = $1 AND ls.conversation_id = n.conversation_id AND ls.last_seen_at >= m.created_at
)
INSERT INTO conversation_last_seen (user_id, conversation_id, last_seen_at, updated_at)
SELECT $1, (SELECT id FROM target), ts, clock_timestamp() FROM last_msg
ON CONFLICT (conversation_id, user_id)
DO UPDATE SET
last_seen_at = EXCLUDED.last_seen_at,
updated_at = GREATEST(conversation_last_seen.updated_at + INTERVAL '1 microsecond', clock_timestamp())
RETURNING last_seen_at, (EXTRACT(EPOCH FROM updated_at) * 1000000)::bigint AS read_version;
-- name: get-active-livechat-conversations-by-agent
SELECT c.uuid, c.contact_id, c.inbox_id
FROM conversations c
JOIN inboxes i ON i.id = c.inbox_id
JOIN users u ON u.id = c.contact_id
WHERE c.assigned_user_id = $1
AND i.channel = 'livechat'
AND i.enabled = TRUE
AND i.deleted_at IS NULL
AND u.availability_status = 'online'
ORDER BY c.last_interaction_at DESC
LIMIT 50;
-- name: filter-authorized-list-uuids
-- $1: uuids (uuid[])
-- $2: user_id
-- $3: team_ids (int[])
-- $4: has 'conversations:read'
-- $5: has 'conversations:read_all'
-- $6: has 'conversations:read_assigned'
-- $7: has 'conversations:read_team_all'
-- $8: has 'conversations:read_team_inbox'
-- $9: has 'conversations:read_unassigned'
SELECT uuid::text
FROM conversations
WHERE uuid = ANY($1::uuid[])
AND $4
AND (
$5
OR ($6 AND assigned_user_id = $2)
OR ($7 AND assigned_team_id = ANY($3::int[]))
OR ($8 AND assigned_team_id = ANY($3::int[]) AND assigned_user_id IS NULL)
OR ($9 AND assigned_user_id IS NULL AND assigned_team_id IS NULL)
);
-- name: get-conversation-uuids-by-contact
SELECT uuid::text
FROM conversations
WHERE contact_id = $1
ORDER BY last_message_at DESC NULLS LAST
LIMIT 200;
-- name: get-conversation-uuids-by-contact-inbox
SELECT uuid::text
FROM conversations
WHERE contact_id = $1 AND inbox_id = $2
ORDER BY last_message_at DESC NULLS LAST
LIMIT 200;
-- name: lock-campaign-delivery
SELECT COALESCE(conversation_uuid::text, '') FROM widget_campaign_deliveries WHERE id = $1 FOR UPDATE;
-- name: complete-campaign-delivery
UPDATE widget_campaign_deliveries
SET conversation_uuid = $2, contact_id = $3, replied = TRUE, opened = TRUE, displayed = TRUE
WHERE id = $1;
-- name: assign-proactive-team
UPDATE conversations SET assigned_team_id = NULLIF($2, 0) WHERE id = $1;