1import type * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import type { Room } from "./Room.ts";
4
5/**
6 * The payload Amazon IVS Chat sends to a room's message review handler for
7 * every `SendMessage` request — the wire shape is PascalCase, verbatim from
8 * the [message review handler contract](https://docs.aws.amazon.com/ivs/latest/ChatUserGuide/chat-message-review-handler.html).
9 */
10export interface RoomMessageEvent {
11 /** Original content of the message. */
12 Content: string;
13 /** The message ID, generated by IVS Chat. */
14 MessageId: string;
15 /** ARN of the room the message was sent to. */
16 RoomArn: string;
17 /** Attributes associated with the message. */
18 Attributes?: { [key: string]: string | undefined };
19 /** Information about the sender. */
20 Sender?: {
21 /** The `userId` the sender's chat token was minted with. */
22 UserId?: string;
23 /** IP address of the sender. */
24 Ip?: string;
25 /** The attributes attached to the sender's chat token. */
26 Attributes?: { [key: string]: string | undefined };
27 };
28}
29
30/**
31 * The review verdict a message review handler returns to Amazon IVS Chat.
32 * `ALLOW` delivers the message (with the possibly-edited `Content` /
33 * `Attributes`); `DENY` drops it — the sender receives a WebSocket 406
34 * whose message carries `Attributes.Reason` when provided.
35 */
36export interface RoomMessageReview {
37 /** Whether to deliver the message. */
38 ReviewResult: "ALLOW" | "DENY";
39 /**
40 * Content to deliver — edited or original. Defaults to the original
41 * message content when omitted.
42 */
43 Content?: string;
44 /**
45 * Attributes to deliver with the message. On `DENY`, a `Reason` entry is
46 * surfaced to the sender in the WebSocket 406 error message.
47 */
48 Attributes?: { [key: string]: string | undefined };
49}
50
51/**
52 * A message review handler receives each message sent to the bound room and
53 * returns the review verdict. Returning `void` allows the message with its
54 * original content and attributes.
55 */
56export type RoomMessageReviewHandlerFn<Req = never> = (
57 event: RoomMessageEvent,
58) => Effect.Effect<RoomMessageReview | void, never, Req>;
59
60export interface RoomMessageReviewProps {
61 /**
62 * What happens to a message if the handler errors, times out, or returns
63 * an invalid response: `ALLOW` delivers it, `DENY` drops it.
64 * @default "ALLOW"
65 */
66 fallbackResult?: "ALLOW" | "DENY";
67}
68
69export type RoomMessageReviewEventSourceService = <Req = never>(
70 room: Room,
71 handler: RoomMessageReviewHandlerFn<Req>,
72 props?: RoomMessageReviewProps,
73) => Effect.Effect<void, never, never>;
74
75/**
76 * Event source connecting an IVS Chat {@link Room}'s message review handler
77 * to the hosting Lambda function — every message sent to the room is
78 * synchronously reviewed (allow / modify / deny) by the handler before
79 * delivery.
80 *
81 * At deploy time the Lambda implementation
82 * (`Lambda.RoomMessageReviewEventSource`) injects the function ARN into the
83 * room's `messageReviewHandler` (via the room's binding contract) and
84 * creates the `lambda:InvokeFunction` Permission for
85 * `ivschat.amazonaws.com`; at runtime it dispatches review invocations for
86 * the bound room to the handler and returns the verdict to IVS Chat.
87 *
88 * Use the {@link onReviewMessage} helper rather than the service directly,
89 * and provide `Lambda.RoomMessageReviewEventSource` on the hosting
90 * function.
91 * ### Reviewing Messages
92 * **Example:** Moderate a Room from a Lambda Function
93 * ```typescript
94 * export default ChatFunction.make(
95 * { main: import.meta.url },
96 * Effect.gen(function* () {
97 * const room = yield* IVSChat.Room("LiveChat");
98 *
99 * // deploy: sets the room's messageReviewHandler + invoke Permission
100 * // runtime: reviews every message sent to the room
101 * yield* IVSChat.onReviewMessage(room, (event) =>
102 * Effect.succeed(
103 * event.Content.includes("banned-word")
104 * ? { ReviewResult: "DENY", Attributes: { Reason: "moderated" } }
105 * : { ReviewResult: "ALLOW", Content: event.Content.trim() },
106 * ),
107 * );
108 *
109 * return {};
110 * }).pipe(Effect.provide(Lambda.RoomMessageReviewEventSource)),
111 * );
112 * ```
113 *
114 * @binding
115 */
116export interface RoomMessageReviewEventSource extends Binding.Service<
117 RoomMessageReviewEventSource,
118 "AWS.IVSChat.RoomMessageReviewEventSource",
119 RoomMessageReviewEventSourceService
120> {}
121
122export const RoomMessageReviewEventSource =
123 Binding.Service<RoomMessageReviewEventSource>(
124 "AWS.IVSChat.RoomMessageReviewEventSource",
125 );
126
127/**
128 * Review (and optionally modify or deny) every message sent to the room
129 * with the current Lambda function.
130 *
131 * Provide `Lambda.RoomMessageReviewEventSource` on the hosting function to
132 * satisfy the requirement.
133 *
134 * @param room The room whose messages to review.
135 * @param handler Invoked once per message; the returned
136 * {@link RoomMessageReview} is the verdict IVS Chat applies. `void` allows
137 * the message unchanged.
138 * @param props Optional `fallbackResult` applied when the handler errors or
139 * times out.
140 *
141 * @example Deny messages containing a banned word
142 * ```typescript
143 * yield* IVSChat.onReviewMessage(
144 * room,
145 * (event) =>
146 * Effect.succeed(
147 * event.Content.includes("banned-word")
148 * ? { ReviewResult: "DENY", Attributes: { Reason: "moderated" } }
149 * : undefined,
150 * ),
151 * { fallbackResult: "ALLOW" },
152 * );
153 * ```
154 */
155export function onReviewMessage<Req = never>(
156 room: Room,
157 handler: RoomMessageReviewHandlerFn<Req>,
158 props?: RoomMessageReviewProps,
159): Effect.Effect<void, never, RoomMessageReviewEventSource> {
160 return RoomMessageReviewEventSource.use((source) =>
161 source(room, handler, props),
162 );
163}