Skip to main content

kick_api/models/
channel_info.rs

1use serde::Deserialize;
2
3/// Public channel information from Kick's v2 API.
4///
5/// Returned by [`fetch_channel_info`](crate::fetch_channel_info). Contains
6/// chatroom settings, subscriber badges, user profile, and livestream status.
7/// No authentication is required.
8///
9/// # Example
10///
11/// ```no_run
12/// use kick_api::fetch_channel_info;
13///
14/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
15/// let info = fetch_channel_info("xqc").await?;
16/// println!("Chatroom ID: {}", info.chatroom.id);
17/// println!("Followers: {}", info.followers_count);
18/// if let Some(stream) = &info.livestream {
19///     println!("Live: {} viewers", stream.viewer_count);
20/// }
21/// # Ok(())
22/// # }
23/// ```
24#[derive(Debug, Clone, Deserialize)]
25pub struct ChannelInfo {
26    /// Channel ID
27    pub id: u64,
28
29    /// User ID of the broadcaster
30    pub user_id: u64,
31
32    /// Channel URL slug
33    pub slug: String,
34
35    /// Whether the channel is banned
36    #[serde(default)]
37    pub is_banned: bool,
38
39    /// Whether VODs are enabled
40    #[serde(default)]
41    pub vod_enabled: bool,
42
43    /// Whether subscriptions are enabled
44    #[serde(default)]
45    pub subscription_enabled: bool,
46
47    /// Whether the channel is a Kick affiliate
48    #[serde(default)]
49    pub is_affiliate: bool,
50
51    /// Whether the channel is verified
52    #[serde(default)]
53    pub verified: bool,
54
55    /// Number of followers
56    #[serde(default)]
57    pub followers_count: u64,
58
59    /// Whether the channel can host other channels
60    #[serde(default)]
61    pub can_host: bool,
62
63    /// Chatroom settings
64    pub chatroom: ChatroomInfo,
65
66    /// Subscriber badge tiers
67    #[serde(default)]
68    pub subscriber_badges: Vec<SubscriberBadge>,
69
70    /// Broadcaster's user profile
71    #[serde(default)]
72    pub user: Option<ChannelUser>,
73
74    /// Current livestream info (None if offline)
75    #[serde(default)]
76    pub livestream: Option<LivestreamInfo>,
77}
78
79/// Chatroom settings for a channel.
80#[derive(Debug, Clone, Deserialize)]
81pub struct ChatroomInfo {
82    /// Unique chatroom identifier (used for WebSocket subscription)
83    pub id: u64,
84
85    /// Channel ID this chatroom belongs to
86    #[serde(default)]
87    pub channel_id: Option<u64>,
88
89    /// Chat mode: "public", "followers", "subscribers"
90    #[serde(default)]
91    pub chat_mode: Option<String>,
92
93    /// Whether slow mode is enabled
94    #[serde(default)]
95    pub slow_mode: bool,
96
97    /// Whether followers-only mode is enabled
98    #[serde(default)]
99    pub followers_mode: bool,
100
101    /// Whether subscribers-only mode is enabled
102    #[serde(default)]
103    pub subscribers_mode: bool,
104
105    /// Whether emotes-only mode is enabled
106    #[serde(default)]
107    pub emotes_mode: bool,
108
109    /// Minimum seconds between messages (slow mode interval)
110    #[serde(default)]
111    pub message_interval: u32,
112
113    /// Minimum follow duration in minutes to chat (when followers mode is on)
114    #[serde(default)]
115    pub following_min_duration: u32,
116}
117
118/// A subscriber badge tier for a channel.
119#[derive(Debug, Clone, Deserialize)]
120pub struct SubscriberBadge {
121    /// Badge ID
122    pub id: u64,
123
124    /// Channel ID this badge belongs to
125    #[serde(default)]
126    pub channel_id: Option<u64>,
127
128    /// Number of subscription months required for this badge
129    pub months: u32,
130
131    /// Badge image URLs
132    pub badge_image: BadgeImage,
133}
134
135/// Image URLs for a subscriber badge.
136#[derive(Debug, Clone, Deserialize)]
137pub struct BadgeImage {
138    /// Primary image URL
139    pub src: String,
140
141    /// Srcset for responsive images
142    #[serde(default)]
143    pub srcset: Option<String>,
144}
145
146/// Broadcaster's user profile.
147#[derive(Debug, Clone, Deserialize)]
148pub struct ChannelUser {
149    /// User ID
150    pub id: u64,
151
152    /// Display username
153    pub username: String,
154
155    /// User bio/description
156    #[serde(default)]
157    pub bio: Option<String>,
158
159    /// Profile picture URL
160    #[serde(default)]
161    pub profile_pic: Option<String>,
162
163    /// Social media links
164    #[serde(default)]
165    pub instagram: Option<String>,
166    #[serde(default)]
167    pub twitter: Option<String>,
168    #[serde(default)]
169    pub youtube: Option<String>,
170    #[serde(default)]
171    pub discord: Option<String>,
172    #[serde(default)]
173    pub tiktok: Option<String>,
174}
175
176/// Current livestream information.
177#[derive(Debug, Clone, Deserialize)]
178pub struct LivestreamInfo {
179    /// Livestream ID
180    pub id: u64,
181
182    /// Stream slug
183    #[serde(default)]
184    pub slug: Option<String>,
185
186    /// Channel ID
187    #[serde(default)]
188    pub channel_id: Option<u64>,
189
190    /// Stream title
191    #[serde(default)]
192    pub session_title: Option<String>,
193
194    /// Whether the stream is currently live
195    #[serde(default)]
196    pub is_live: bool,
197
198    /// Whether the stream is marked as mature
199    #[serde(default)]
200    pub is_mature: bool,
201
202    /// Stream language (e.g., "English")
203    #[serde(default)]
204    pub language: Option<String>,
205
206    /// Current viewer count
207    #[serde(default)]
208    pub viewer_count: u64,
209
210    /// When the stream started (ISO 8601)
211    #[serde(default)]
212    pub start_time: Option<String>,
213
214    /// Stream tags
215    #[serde(default)]
216    pub tags: Vec<String>,
217
218    /// Stream categories
219    #[serde(default)]
220    pub categories: Vec<LivestreamCategory>,
221}
222
223/// Category information for a livestream.
224#[derive(Debug, Clone, Deserialize)]
225pub struct LivestreamCategory {
226    /// Category ID
227    pub id: u64,
228
229    /// Category name (e.g., "Just Chatting")
230    pub name: String,
231
232    /// Category URL slug
233    pub slug: String,
234
235    /// Category tags
236    #[serde(default)]
237    pub tags: Vec<String>,
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_deserialize_channel_info() {
246        let json = r##"{
247            "id": 44192640,
248            "user_id": 45297540,
249            "slug": "hello_kiko",
250            "is_banned": false,
251            "vod_enabled": true,
252            "subscription_enabled": true,
253            "is_affiliate": true,
254            "verified": true,
255            "followers_count": 8393,
256            "can_host": true,
257            "chatroom": {
258                "id": 43904307,
259                "channel_id": 44192640,
260                "chat_mode": "public",
261                "slow_mode": false,
262                "followers_mode": false,
263                "subscribers_mode": false,
264                "emotes_mode": false,
265                "message_interval": 5,
266                "following_min_duration": 0
267            },
268            "subscriber_badges": [
269                {
270                    "id": 499365,
271                    "channel_id": 44192640,
272                    "months": 1,
273                    "badge_image": {
274                        "srcset": "",
275                        "src": "https://files.kick.com/channel_subscriber_badges/499365/original"
276                    }
277                },
278                {
279                    "id": 499366,
280                    "channel_id": 44192640,
281                    "months": 2,
282                    "badge_image": {
283                        "srcset": "",
284                        "src": "https://files.kick.com/channel_subscriber_badges/499366/original"
285                    }
286                }
287            ],
288            "user": {
289                "id": 45297540,
290                "username": "hello_kiko",
291                "bio": "Hi I'm Kiko",
292                "profile_pic": "https://files.kick.com/images/user/45297540/profile_image.webp",
293                "instagram": "bye.kiko",
294                "twitter": "hello_kiko",
295                "youtube": "hello_kiko",
296                "discord": "hellokiko",
297                "tiktok": "bye.kiko"
298            },
299            "livestream": {
300                "id": 103692434,
301                "slug": "some-stream-slug",
302                "channel_id": 44192640,
303                "session_title": "Just Chatting stream!",
304                "is_live": true,
305                "is_mature": false,
306                "language": "English",
307                "viewer_count": 99,
308                "start_time": "2026-04-06 05:03:57",
309                "tags": ["Japan", "Korean"],
310                "categories": [
311                    {
312                        "id": 15,
313                        "name": "Just Chatting",
314                        "slug": "just-chatting",
315                        "tags": ["IRL", "Casual"]
316                    }
317                ]
318            }
319        }"##;
320
321        let info: ChannelInfo = serde_json::from_str(json).unwrap();
322
323        // Channel basics
324        assert_eq!(info.id, 44192640);
325        assert_eq!(info.user_id, 45297540);
326        assert_eq!(info.slug, "hello_kiko");
327        assert!(!info.is_banned);
328        assert!(info.verified);
329        assert!(info.is_affiliate);
330        assert_eq!(info.followers_count, 8393);
331
332        // Chatroom
333        assert_eq!(info.chatroom.id, 43904307);
334        assert_eq!(info.chatroom.chat_mode, Some("public".into()));
335        assert!(!info.chatroom.slow_mode);
336        assert!(!info.chatroom.followers_mode);
337        assert!(!info.chatroom.subscribers_mode);
338        assert_eq!(info.chatroom.message_interval, 5);
339
340        // Subscriber badges
341        assert_eq!(info.subscriber_badges.len(), 2);
342        assert_eq!(info.subscriber_badges[0].months, 1);
343        assert!(info.subscriber_badges[0].badge_image.src.contains("499365"));
344        assert_eq!(info.subscriber_badges[1].months, 2);
345
346        // User profile
347        let user = info.user.unwrap();
348        assert_eq!(user.username, "hello_kiko");
349        assert_eq!(user.bio, Some("Hi I'm Kiko".into()));
350        assert_eq!(user.discord, Some("hellokiko".into()));
351
352        // Livestream
353        let stream = info.livestream.unwrap();
354        assert!(stream.is_live);
355        assert_eq!(stream.viewer_count, 99);
356        assert_eq!(stream.session_title, Some("Just Chatting stream!".into()));
357        assert_eq!(stream.tags, vec!["Japan", "Korean"]);
358        assert_eq!(stream.categories.len(), 1);
359        assert_eq!(stream.categories[0].name, "Just Chatting");
360    }
361
362    #[test]
363    fn test_deserialize_offline_channel() {
364        let json = r##"{
365            "id": 12345,
366            "user_id": 67890,
367            "slug": "offline_user",
368            "chatroom": {
369                "id": 11111,
370                "slow_mode": true,
371                "followers_mode": true,
372                "subscribers_mode": false,
373                "emotes_mode": false,
374                "message_interval": 10,
375                "following_min_duration": 30
376            },
377            "subscriber_badges": [],
378            "livestream": null
379        }"##;
380
381        let info: ChannelInfo = serde_json::from_str(json).unwrap();
382        assert_eq!(info.slug, "offline_user");
383        assert!(info.livestream.is_none());
384        assert!(info.chatroom.slow_mode);
385        assert!(info.chatroom.followers_mode);
386        assert_eq!(info.chatroom.message_interval, 10);
387        assert_eq!(info.chatroom.following_min_duration, 30);
388        assert!(info.subscriber_badges.is_empty());
389        assert!(info.user.is_none());
390    }
391
392    #[test]
393    fn test_deserialize_minimal_channel() {
394        // Only required fields — everything else defaults
395        let json = r##"{
396            "id": 1,
397            "user_id": 2,
398            "slug": "test",
399            "chatroom": { "id": 3 }
400        }"##;
401
402        let info: ChannelInfo = serde_json::from_str(json).unwrap();
403        assert_eq!(info.id, 1);
404        assert_eq!(info.chatroom.id, 3);
405        assert!(!info.chatroom.slow_mode);
406        assert_eq!(info.followers_count, 0);
407        assert!(!info.verified);
408        assert!(info.subscriber_badges.is_empty());
409    }
410}