1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#![doc(alias = "channel.chat.message")]
//! Any user sends a message to a specific chat room.
use super::*;
/// [`channel.chat.message`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelchatmessage): a user sends a message to a specific chat room.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct ChannelChatMessageV1 {
/// User ID of the channel to receive chat message events for.
#[cfg_attr(feature = "typed-builder", builder(setter(into)))]
pub broadcaster_user_id: types::UserId,
/// The user ID to read chat as.
#[cfg_attr(feature = "typed-builder", builder(setter(into)))]
pub user_id: types::UserId,
}
impl ChannelChatMessageV1 {
/// Create a new [ChannelChatMessageV1]
pub fn new(
broadcaster_user_id: impl Into<types::UserId>,
user_id: impl Into<types::UserId>,
) -> Self {
Self {
broadcaster_user_id: broadcaster_user_id.into(),
user_id: user_id.into(),
}
}
}
impl EventSubscription for ChannelChatMessageV1 {
type Payload = ChannelChatMessageV1Payload;
const EVENT_TYPE: EventType = EventType::ChannelChatMessage;
#[cfg(feature = "twitch_oauth2")]
/// Additionally, if an app access token is used,
/// [user:bot][twitch_oauth2::Scope::UserBot] is requried from the chatting user, i.e. the user specified by [user_id][ChannelChatMessageV1::user_id],
/// and either [channel:bot][twitch_oauth2::Scope::ChannelBot] from the broadcaster or moderator status in chat.
const SCOPE: twitch_oauth2::Validator =
twitch_oauth2::validator![twitch_oauth2::Scope::UserReadChat];
const VERSION: &'static str = "1";
}
/// [`channel.chat.message`](ChannelChatMessageV1Payload) response payload.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct ChannelChatMessageV1Payload {
/// The broadcaster user ID.
pub broadcaster_user_id: types::UserId,
/// The broadcaster display name.
pub broadcaster_user_name: types::DisplayName,
/// The broadcaster login.
pub broadcaster_user_login: types::UserName,
/// The user ID of the user that sent the message.
pub chatter_user_id: types::UserId,
/// The user name of the user that sent the message.
pub chatter_user_name: types::DisplayName,
/// The user login of the user that sent the message.
pub chatter_user_login: types::UserName,
/// A UUID that identifies the message.
pub message_id: types::MsgId,
/// The structured chat message.
pub message: Message,
/// The type of message.
pub message_type: MessageType,
/// List of chat badges.
pub badges: Vec<Badge>,
/// Metadata if this message is a cheer.
pub cheer: Option<Cheer>,
/// The color of the user's name in the chat room.
/// This is a hexadecimal RGB color code in the form, `#<RGB>`.
/// This may be empty if it is never set.
pub color: types::HexColor,
/// Metadata if this message is a reply.
pub reply: Option<Reply>,
/// The ID of a channel points custom reward that was redeemed.
pub channel_points_custom_reward_id: Option<types::RewardId>,
/// An ID for the type of animation selected as part of an “animate my message” redemption.
pub channel_points_animation_id: Option<String>,
/// Only present when in a shared chat session. The broadcaster user ID of the channel the message was sent from.
pub source_broadcaster_user_id: Option<types::UserId>,
/// Only present when in a shared chat session. The user name of the broadcaster of the channel the message was sent from.
pub source_broadcaster_user_name: Option<types::DisplayName>,
/// Only present when in a shared chat session. The login of the broadcaster of the channel the message was sent from.
pub source_broadcaster_user_login: Option<types::UserName>,
/// Only present when in a shared chat session. The UUID that identifies the source message from the channel the message was sent from.
pub source_message_id: Option<types::MsgId>,
/// Only present when in a shared chat session. The list of chat badges for the chatter in the channel the message was sent from.
#[serde(deserialize_with = "crate::deserialize_default_from_null")]
pub source_badges: Vec<Badge>,
}
/// The type a message.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum MessageType {
/// A regular text message
Text,
/// A highlighted message with channel points
ChannelPointsHighlighted,
/// A message sent with channel points during sub-only mode
ChannelPointsSubOnly,
/// A first message from a user
UserIntro,
/// A gigantified emote
PowerUpsGigantifiedEmote,
/// A message sent with effects
PowerUpsMessageEffect,
}
/// Chat badge
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Badge {
/// An ID that identifies this set of chat badges. For example, Bits or Subscriber.
pub set_id: types::BadgeSetId,
/// An ID that identifies this version of the badge. The ID can be any value.
/// For example, for Bits, the ID is the Bits tier level, but for World of Warcraft, it could be Alliance or Horde.
pub id: types::ChatBadgeId,
/// Contains metadata related to the chat badges in the badges tag.
/// Currently, this tag contains metadata only for subscriber badges, to indicate the number of months the user has been a subscriber.
pub info: String,
}
/// Metadata for cheer messages
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Cheer {
/// The amount of Bits the user cheered.
pub bits: usize,
}
/// Metadata for reply messages
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Reply {
/// An ID that uniquely identifies the parent message that this message is replying to.
pub parent_message_id: types::MsgId,
/// The message body of the parent message.
pub parent_message_body: String,
/// User ID of the sender of the parent message.
pub parent_user_id: types::UserId,
/// User name of the sender of the parent message.
pub parent_user_name: types::DisplayName,
/// User login of the sender of the parent message.
pub parent_user_login: types::UserName,
/// An ID that identifies the parent message of the reply thread.
pub thread_message_id: types::MsgId,
/// User ID of the sender of the thread's parent message.
pub thread_user_id: types::UserId,
/// User name of the sender of the thread's parent message.
pub thread_user_name: types::DisplayName,
/// User login of the sender of the thread's parent message.
pub thread_user_login: types::UserName,
}
#[cfg(test)]
#[test]
fn parse_payload() {
let payload = r##"
{
"subscription": {
"id": "0b7f3361-672b-4d39-b307-dd5b576c9b27",
"status": "enabled",
"type": "channel.chat.message",
"version": "1",
"condition": {
"broadcaster_user_id": "1971641",
"user_id": "2914196"
},
"transport": {
"method": "websocket",
"session_id": "AgoQHR3s6Mb4T8GFB1l3DlPfiRIGY2VsbC1h"
},
"created_at": "2023-11-06T18:11:47.492253549Z",
"cost": 0
},
"event": {
"broadcaster_user_id": "1971641",
"broadcaster_user_login": "streamer",
"broadcaster_user_name": "streamer",
"chatter_user_id": "4145994",
"chatter_user_login": "viewer32",
"chatter_user_name": "viewer32",
"message_id": "cc106a89-1814-919d-454c-f4f2f970aae7",
"message": {
"text": "Hi chat",
"fragments": [
{
"type": "text",
"text": "Hi chat",
"cheermote": null,
"emote": null,
"mention": null
}
]
},
"color": "#00FF7F",
"badges": [
{
"set_id": "moderator",
"id": "1",
"info": ""
},
{
"set_id": "subscriber",
"id": "12",
"info": "16"
},
{
"set_id": "sub-gifter",
"id": "1",
"info": ""
}
],
"message_type": "text",
"cheer": null,
"reply": null,
"channel_points_custom_reward_id": null,
"source_broadcaster_user_id": null,
"source_broadcaster_user_login": null,
"source_broadcaster_user_name": null,
"source_message_id": null,
"source_badges": null
}
}
"##;
let val = dbg!(crate::eventsub::Event::parse(payload).unwrap());
crate::tests::roundtrip(&val)
}
#[cfg(test)]
#[test]
fn parse_payload_shared() {
let payload = r##"
{
"subscription": {
"id": "0b7f3361-672b-4d39-b307-dd5b576c9b27",
"status": "enabled",
"type": "channel.chat.message",
"version": "1",
"condition": {
"broadcaster_user_id": "1971641",
"user_id": "2914196"
},
"transport": {
"method": "websocket",
"session_id": "AgoQHR3s6Mb4T8GFB1l3DlPfiRIGY2VsbC1h"
},
"created_at": "2023-11-06T18:11:47.492253549Z",
"cost": 0
},
"event": {
"broadcaster_user_id": "1971641",
"broadcaster_user_login": "streamer",
"broadcaster_user_name": "streamer",
"chatter_user_id": "4145994",
"chatter_user_login": "viewer32",
"chatter_user_name": "viewer32",
"message_id": "cc106a89-1814-919d-454c-f4f2f970aae7",
"message": {
"text": "Hi chat",
"fragments": [
{
"type": "text",
"text": "Hi chat",
"cheermote": null,
"emote": null,
"mention": null
}
]
},
"color": "#00FF7F",
"badges": [
{
"set_id": "moderator",
"id": "1",
"info": ""
},
{
"set_id": "subscriber",
"id": "12",
"info": "16"
},
{
"set_id": "sub-gifter",
"id": "1",
"info": ""
}
],
"message_type": "text",
"cheer": null,
"reply": null,
"channel_points_custom_reward_id": null,
"source_broadcaster_user_id": "112233",
"source_broadcaster_user_login": "streamer33",
"source_broadcaster_user_name": "streamer33",
"source_message_id": "e03f6d5d-8ec8-4c63-b473-9e5fe61e289b",
"source_badges": [
{
"set_id": "subscriber",
"id": "3",
"info": "3"
}
]
}
}
"##;
let val = dbg!(crate::eventsub::Event::parse(payload).unwrap());
crate::tests::roundtrip(&val)
}