Skip to content

Commit 047539d

Browse files
authored
Merge pull request #3736 from techmatters/CHI-3571-zero_retention
CHI-3571: Zero transcript retention support
2 parents 8d6e50a + 8c19ac5 commit 047539d

File tree

10 files changed

+430
-17
lines changed

10 files changed

+430
-17
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Copyright (C) 2021-2025 Technology Matters
3+
* This program is free software: you can redistribute it and/or modify
4+
* it under the terms of the GNU Affero General Public License as published
5+
* by the Free Software Foundation, either version 3 of the License, or
6+
* (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU Affero General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU Affero General Public License
14+
* along with this program. If not, see https://www.gnu.org/licenses/.
15+
*/
16+
17+
import {
18+
registerServiceScopedConversationEventHandler,
19+
ServiceScopedConversationEventHandler,
20+
} from './serviceScopedConversationEventHandler';
21+
import type { AccountSID } from '@tech-matters/twilio-types';
22+
import { Twilio } from 'twilio';
23+
import {
24+
CHANNEL_UPDATED,
25+
ChannelUpdatedEvent,
26+
ConversationStateUpdatedEvent,
27+
} from './eventTypes';
28+
import { retrieveServiceConfigurationAttributes } from '../configuration/aseloConfiguration';
29+
import { getChatServiceSid } from '@tech-matters/twilio-configuration';
30+
31+
const deleteClosedConversationEventHandler: ServiceScopedConversationEventHandler =
32+
async (
33+
{
34+
StateTo: stateTo,
35+
StateFrom: stateFrom,
36+
ConversationSid: conversationSid,
37+
}: ConversationStateUpdatedEvent,
38+
accountSid: AccountSID,
39+
client: Twilio,
40+
): Promise<void> => {
41+
const serviceConfigAttributes = await retrieveServiceConfigurationAttributes(client);
42+
43+
const zeroTranscriptRetention = Boolean(
44+
serviceConfigAttributes.enforceZeroTranscriptRetention || 'false',
45+
);
46+
if (zeroTranscriptRetention && stateTo === 'closed') {
47+
console.debug(
48+
`Conversation state for ${conversationSid} updated from ${stateFrom} to ${stateTo} and account ${accountSid} has a zero transcript retention policy, deleting conversation`,
49+
);
50+
await client.conversations.v1.conversations
51+
.get(conversationSid)
52+
.remove({ xTwilioWebhookEnabled: 'true' });
53+
console.debug(
54+
`Deleted ${conversationSid} when it's state was updated from ${stateFrom} to ${stateTo} because account ${accountSid} has a zero transcript retention policy.`,
55+
);
56+
}
57+
};
58+
59+
registerServiceScopedConversationEventHandler(
60+
['onConversationStateUpdated'],
61+
deleteClosedConversationEventHandler,
62+
);
63+
64+
const deleteInactiveChatChannelEventHandler: ServiceScopedConversationEventHandler =
65+
async (
66+
{ Attributes: attributesJson, ChannelSid: channelSid }: ChannelUpdatedEvent,
67+
accountSid: AccountSID,
68+
client: Twilio,
69+
): Promise<void> => {
70+
const serviceConfigAttributes = await retrieveServiceConfigurationAttributes(client);
71+
72+
const zeroTranscriptRetention = Boolean(
73+
serviceConfigAttributes.enforceZeroTranscriptRetention || 'false',
74+
);
75+
if (zeroTranscriptRetention) {
76+
const { status } = JSON.parse(attributesJson || '{}');
77+
if (status === 'INACTIVE') {
78+
console.debug(
79+
`Status attribute for ${channelSid} set to INACTIVE and account ${accountSid} has a zero transcript retention policy, deleting conversation`,
80+
);
81+
await client.chat.v2.services
82+
.get(await getChatServiceSid(accountSid))
83+
.channels.get(channelSid)
84+
.remove({ xTwilioWebhookEnabled: 'true' });
85+
console.debug(
86+
`Deleted ${channelSid} when it's status attribute was set to INACTIVE because account ${accountSid} has a zero transcript retention policy.`,
87+
);
88+
}
89+
}
90+
};
91+
92+
registerServiceScopedConversationEventHandler(
93+
[CHANNEL_UPDATED],
94+
deleteInactiveChatChannelEventHandler,
95+
);
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright (C) 2021-2025 Technology Matters
3+
* This program is free software: you can redistribute it and/or modify
4+
* it under the terms of the GNU Affero General Public License as published
5+
* by the Free Software Foundation, either version 3 of the License, or
6+
* (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU Affero General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU Affero General Public License
14+
* along with this program. If not, see https://www.gnu.org/licenses/.
15+
*/
16+
17+
import { ChatChannelSID, ConversationSID } from '@tech-matters/twilio-types';
18+
19+
/**
20+
* Conversation Event Types: https://www.twilio.com/docs/taskrouter/api/event/reference#event-types
21+
*/
22+
23+
// Conversations
24+
export const CONVERSATION_ADD = 'onConversationAdd'; // Fires when a new conversation is created.
25+
export const CONVERSATION_REMOVE = 'onConversationRemove'; // Fires when a conversation is removed from the Service.
26+
export const CONVERSATION_UPDATE = 'onConversationUpdate'; // Fires when any attribute of a conversation is changed.
27+
28+
export const MESSAGE_ADD = 'onMessageAdd'; // Fires when a new message is posted to a conversation.
29+
export const MESSAGE_REMOVE = 'onMessageRemove'; // Fires when a message is deleted from a conversation.
30+
export const MESSAGE_UPDATE = 'onMessageUpdate'; // Fires when a posted message's body or any attribute is changed.
31+
32+
export const PARTICIPANT_ADD = 'onParticipantAdd'; // Fires when a Participant has joined a Conversation as a Member.
33+
export const PARTICIPANT_REMOVE = 'onParticipantRemove'; // Fires when a User is removed from the set of Conversation Members.
34+
export const PARTICIPANT_UPDATE = 'onParticipantUpdate'; // Fires when any configurable attribute of a User is changed. Will not be fired for reachability events.
35+
36+
export const CONVERSATION_ADDED = 'onConversationAdded'; // Fires when a new conversation is created.
37+
export const CONVERSATION_REMOVED = 'onConversationRemoved'; // Fires when a conversation is removed from the Service.
38+
export const CONVERSATION_UPDATED = 'onConversationUpdated'; // Fires when any attribute of a conversation is changed.
39+
export const CONVERSATION_STATE_UPDATED = 'onConversationStateUpdated'; // Fires when the state of a Conversation is updated, e.g., from "active" to "inactive"
40+
41+
export const MESSAGE_ADDED = 'onMessageAdded'; // Fires when a new message is posted to a conversation.
42+
export const MESSAGE_REMOVED = 'onMessageRemoved'; // Fires when a message is deleted from a conversation.
43+
export const MESSAGE_UPDATED = 'onMessageUpdated'; // Fires when a posted message's body or any attribute is changed.
44+
45+
export const PARTICIPANT_ADDED = 'onParticipantAdded'; // Fires when a Participant has joined a Conversation as a Member.
46+
export const PARTICIPANT_REMOVED = 'onParticipantRemoved'; // Fires when a User is removed from the set of Conversation Members.
47+
export const PARTICIPANT_UPDATED = 'onParticipantUpdated'; // Fires when any configurable attribute of a User is changed. Will not be fired for reachability events.
48+
49+
export const DELIVERY_UPDATED = 'onDeliveryUpdated'; // Fires when delivery receipt status is updated
50+
51+
// Programmable Chat
52+
export const MESSAGE_SEND = 'onMessageSend'; // Sending a Message
53+
// export const MESSAGE_UPDATE = 'onMessageUpdate'; // Editing Message Body / Attributes
54+
// export const MESSAGE_REMOVE = 'onMessageRemove'; // Deleting Message
55+
export const MEDIA_MESSAGE_SEND = 'onMediaMessageSend'; // Sending a Media Message
56+
export const CHANNEL_ADD = 'onChannelAdd'; // Creating a Channel
57+
export const CHANNEL_UPDATE = 'onChannelUpdate'; // Edit Channel Properties
58+
export const CHANNEL_UPDATED = 'onChannelUpdated'; // Edited Channel Properties
59+
export const CHANNEL_DESTROY = 'onChannelDestroy'; // Deleting Channel / Destroying Channel
60+
export const MEMBER_ADD = 'onMemberAdd'; // Joining Channel / Channel Member being Added
61+
export const MEMBER_UPDATE = 'onMemberUpdate'; // Channel Member to be Updated
62+
export const MEMBER_REMOVE = 'onMemberRemove'; // Channel Member to be Removed / Channel Member Leaving
63+
export const USER_UPDATE = 'onUserUpdate'; // User to be Updated
64+
65+
export const conversationEventTypes = {
66+
CONVERSATION_ADD,
67+
CONVERSATION_REMOVE,
68+
CONVERSATION_UPDATE,
69+
70+
MESSAGE_ADD,
71+
MESSAGE_REMOVE,
72+
MESSAGE_UPDATE,
73+
74+
PARTICIPANT_ADD,
75+
PARTICIPANT_REMOVE,
76+
PARTICIPANT_UPDATE,
77+
78+
CONVERSATION_ADDED,
79+
CONVERSATION_REMOVED,
80+
CONVERSATION_UPDATED,
81+
CONVERSATION_STATE_UPDATED,
82+
83+
MESSAGE_ADDED,
84+
MESSAGE_REMOVED,
85+
MESSAGE_UPDATED,
86+
87+
PARTICIPANT_ADDED,
88+
PARTICIPANT_REMOVED,
89+
PARTICIPANT_UPDATED,
90+
91+
DELIVERY_UPDATED,
92+
} as const;
93+
94+
export type ConversationEvent =
95+
(typeof conversationEventTypes)[keyof typeof conversationEventTypes];
96+
97+
export const programmableChatEventTypes = {
98+
MESSAGE_SEND,
99+
MESSAGE_UPDATE,
100+
MESSAGE_REMOVE,
101+
MEDIA_MESSAGE_SEND,
102+
CHANNEL_ADD,
103+
CHANNEL_UPDATE,
104+
CHANNEL_UPDATED,
105+
CHANNEL_DESTROY,
106+
MEMBER_ADD,
107+
MEMBER_UPDATE,
108+
MEMBER_REMOVE,
109+
USER_UPDATE,
110+
} as const;
111+
112+
export type ProgrammableChatEvent =
113+
(typeof programmableChatEventTypes)[keyof typeof programmableChatEventTypes];
114+
115+
export type ConversationStateUpdatedEvent = {
116+
EventType: 'onConversationStateUpdated';
117+
ChatServiceSid: string;
118+
StateUpdated: string; // ISO8601 time Modification date of the state
119+
StateFrom: 'active' | 'inactive' | 'closed';
120+
StateTo: 'active' | 'inactive' | 'closed';
121+
ConversationSid: ConversationSID;
122+
Reason: 'API' | 'TIMER' | 'EVENT';
123+
MessagingServiceSid: string;
124+
};
125+
126+
export type ChannelUpdatedEvent = {
127+
EventType: 'onChannelUpdate';
128+
ChannelSid: ChatChannelSID;
129+
Attributes?: string;
130+
DateCreated: string; // The date of creation of the channel
131+
CreatedBy: string;
132+
FriendlyName?: string;
133+
UniqueName?: string;
134+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (C) 2021-2025 Technology Matters
3+
* This program is free software: you can redistribute it and/or modify
4+
* it under the terms of the GNU Affero General Public License as published
5+
* by the Free Software Foundation, either version 3 of the License, or
6+
* (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU Affero General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU Affero General Public License
14+
* along with this program. If not, see https://www.gnu.org/licenses/.
15+
*/
16+
import './unsupportedMediaErrorConversationHandler';
17+
import './deleteClosedConversationHandler';
18+
19+
export { handleConversationEvent } from './serviceScopedConversationEventHandler';
20+
export { conversationEventTypes, ConversationEvent } from './eventTypes';
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Copyright (C) 2021-2025 Technology Matters
3+
* This program is free software: you can redistribute it and/or modify
4+
* it under the terms of the GNU Affero General Public License as published
5+
* by the Free Software Foundation, either version 3 of the License, or
6+
* (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU Affero General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU Affero General Public License
14+
* along with this program. If not, see https://www.gnu.org/licenses/.
15+
*/
16+
17+
import { AccountScopedHandler, HttpRequest } from '../httpTypes';
18+
import { AccountSID } from '@tech-matters/twilio-types';
19+
import twilio from 'twilio';
20+
import { getTwilioClient } from '@tech-matters/twilio-configuration';
21+
import { newOk } from '../Result';
22+
import { ConversationEvent, ProgrammableChatEvent } from './eventTypes';
23+
24+
export type ServiceScopedConversationEventHandler = (
25+
event: any,
26+
accountSid: AccountSID,
27+
twilioClient: twilio.Twilio,
28+
) => Promise<void>;
29+
30+
const eventHandlers: Record<string, ServiceScopedConversationEventHandler[]> = {};
31+
32+
export const registerServiceScopedConversationEventHandler = (
33+
eventTypes: (ConversationEvent | ProgrammableChatEvent)[],
34+
handler: ServiceScopedConversationEventHandler,
35+
) => {
36+
for (const eventType of eventTypes) {
37+
if (!eventHandlers[eventType]) {
38+
eventHandlers[eventType] = [];
39+
}
40+
eventHandlers[eventType].push(handler);
41+
}
42+
};
43+
44+
export const handleConversationEvent: AccountScopedHandler = async (
45+
{ body: event }: HttpRequest,
46+
accountSid: AccountSID,
47+
) => {
48+
console.info(`===== Service Conversation Listener (event: ${event.EventType})=====`);
49+
50+
const handlers = eventHandlers[event.EventType] ?? [];
51+
console.info(
52+
`Handling conversation / programmable chat event: ${event.EventType} for account: ${accountSid} - executing ${handlers.length} registered handlers.`,
53+
);
54+
await Promise.all(
55+
handlers.map(async handler =>
56+
handler(event, accountSid, await getTwilioClient(accountSid)),
57+
),
58+
);
59+
console.debug(
60+
`Successfully executed ${handlers.length} registered handlers for conversation / programmable chat event: ${event.EventType} for account: ${accountSid}.`,
61+
);
62+
return newOk({});
63+
};

0 commit comments

Comments
 (0)