@manaobot/kick 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/biome.json +44 -0
- package/bun.lock +78 -0
- package/example/01-authorize-bot/.env.example +7 -0
- package/example/01-authorize-bot/index.ts +60 -0
- package/example/02-webhook/.env.example +7 -0
- package/example/02-webhook/index.ts +80 -0
- package/example/03-ngrok/.env.example +7 -0
- package/example/03-ngrok/index.ts +92 -0
- package/example/04-categories-api/.env.example +7 -0
- package/example/04-categories-api/index.ts +77 -0
- package/example/05-users-api/.env.example +7 -0
- package/example/05-users-api/index.ts +60 -0
- package/example/06-channels-api/.env.example +7 -0
- package/example/06-channels-api/index.ts +60 -0
- package/example/07-channel-rewards-api/.env.example +7 -0
- package/example/07-channel-rewards-api/index.ts +60 -0
- package/example/08-basic-chat-bot/.env.example +7 -0
- package/example/08-basic-chat-bot/index.ts +102 -0
- package/package.json +23 -0
- package/qodana.yaml +31 -0
- package/src/KickClient.ts +172 -0
- package/src/Logger.ts +25 -0
- package/src/api/CategoriesAPI.ts +45 -0
- package/src/api/ChannelRewardsAPI.ts +121 -0
- package/src/api/ChannelsAPI.ts +63 -0
- package/src/api/KicksAPI.ts +37 -0
- package/src/api/LivestreamsAPI.ts +65 -0
- package/src/api/ModerationAPI.ts +60 -0
- package/src/api/UsersAPI.ts +72 -0
- package/src/auth/AuthManager.ts +64 -0
- package/src/auth/CallbackServer.ts +57 -0
- package/src/auth/OAuth.ts +55 -0
- package/src/auth/PKCE.ts +13 -0
- package/src/auth/TokenManager.ts +53 -0
- package/src/chat/ChatClient.ts +48 -0
- package/src/rest/RestClient.ts +39 -0
- package/src/webhooks/NgrokAdapter.ts +46 -0
- package/src/webhooks/WebhookRouter.ts +135 -0
- package/src/webhooks/WebhookServer.ts +41 -0
- package/tsconfig.json +29 -0
- package/types/api.d.ts +158 -0
- package/types/auth.d.ts +38 -0
- package/types/chat.d.ts +14 -0
- package/types/client.d.ts +67 -0
- package/types/index.d.ts +4 -0
- package/types/webhooks.d.ts +35 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RestClient } from "../rest/RestClient.ts";
|
|
2
|
+
import type { ChatMessage } from "../../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ChatClient provides access to Kick Chat APIs.
|
|
6
|
+
* @example
|
|
7
|
+
* await kick.chat.send({
|
|
8
|
+
* content: "Hello world!"
|
|
9
|
+
* });
|
|
10
|
+
*/
|
|
11
|
+
export class ChatClient {
|
|
12
|
+
constructor(private rest: RestClient) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Send a message to the chat.
|
|
16
|
+
* * Requires scope:
|
|
17
|
+
* `chat:write`
|
|
18
|
+
* @param message The message object to send
|
|
19
|
+
* @returns A promise that resolves when the message is sent
|
|
20
|
+
* @example
|
|
21
|
+
* // Send a simple message
|
|
22
|
+
* await kick.chat.send({ content: "Hello Kick!" });
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
send(message: ChatMessage): Promise<void> {
|
|
26
|
+
return this.rest.fetch("/public/v1/chat", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
type: message.type ?? "bot",
|
|
30
|
+
content: message.content,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Delete a chat message from a channel.
|
|
37
|
+
*
|
|
38
|
+
* * Requires scope:
|
|
39
|
+
* `moderation:chat_message:manage`: Execute moderation actions on chat messages
|
|
40
|
+
*
|
|
41
|
+
* @param messageId Message ID
|
|
42
|
+
*/
|
|
43
|
+
delete(messageId: string): Promise<void> {
|
|
44
|
+
return this.rest.fetch(`/public/v1/chat/${messageId}`, {
|
|
45
|
+
method: "DELETE",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TokenManager } from "../auth/TokenManager.ts";
|
|
2
|
+
|
|
3
|
+
export class RestClient {
|
|
4
|
+
private tokenManager?: TokenManager;
|
|
5
|
+
|
|
6
|
+
setTokenManager(tokenManager: TokenManager) {
|
|
7
|
+
this.tokenManager = tokenManager;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
11
|
+
if (!this.tokenManager) {
|
|
12
|
+
throw new Error("TokenManager not set in RestClient");
|
|
13
|
+
}
|
|
14
|
+
const accessToken = await this.tokenManager.getAccessToken();
|
|
15
|
+
|
|
16
|
+
const response = await fetch(`https://api.kick.com${endpoint}`, {
|
|
17
|
+
...options,
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${accessToken}`,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...(options.headers || {}),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
let suggestion = "";
|
|
27
|
+
if (response.status === 401) {
|
|
28
|
+
suggestion =
|
|
29
|
+
"Have you obtained access token or refreshed them? If so, make sure your scopes cover the requested endpoint.";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error(
|
|
33
|
+
`API request failed, response status: ${response.status}\n[SUGGESTION] ${suggestion}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return response.json() as Promise<T>;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { logger } from "../Logger.ts";
|
|
2
|
+
|
|
3
|
+
type NgrokOptions = {
|
|
4
|
+
port?: number;
|
|
5
|
+
path?: string;
|
|
6
|
+
authtoken?: string;
|
|
7
|
+
domain?: string;
|
|
8
|
+
region?: string;
|
|
9
|
+
onUrl?: (url: string) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function ngrokAdapter(options: NgrokOptions = {}) {
|
|
13
|
+
let ngrok: any;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
ngrok = await import("@ngrok/ngrok");
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"@ngrok/ngrok is not installed. Install it with `npm install @ngrok/ngrok`.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const port = options.port ?? 3000;
|
|
24
|
+
const path = options.path ?? "/kick/webhook";
|
|
25
|
+
|
|
26
|
+
const listener = await ngrok.connect({
|
|
27
|
+
addr: port,
|
|
28
|
+
domain: options.domain,
|
|
29
|
+
region: options.region,
|
|
30
|
+
authtoken: options.authtoken,
|
|
31
|
+
authtoken_from_env: !options.authtoken,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const fullUrl = `${listener.url()}${path}`;
|
|
35
|
+
|
|
36
|
+
options.onUrl?.(fullUrl);
|
|
37
|
+
|
|
38
|
+
logger.success("Ngrok tunnel established", { fullUrl });
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
url: fullUrl,
|
|
42
|
+
async close() {
|
|
43
|
+
await listener.close();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { CreateServerOptions } from "../../types";
|
|
2
|
+
import { webhookServer } from "./WebhookServer.ts";
|
|
3
|
+
import { ngrokAdapter } from "./NgrokAdapter.ts";
|
|
4
|
+
|
|
5
|
+
const KICK_PUBLIC_KEY_PEM = `
|
|
6
|
+
-----BEGIN PUBLIC KEY-----
|
|
7
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq/+l1WnlRrGSolDMA+A8
|
|
8
|
+
6rAhMbQGmQ2SapVcGM3zq8ANXjnhDWocMqfWcTd95btDydITa10kDvHzw9WQOqp2
|
|
9
|
+
MZI7ZyrfzJuz5nhTPCiJwTwnEtWft7nV14BYRDHvlfqPUaZ+1KR4OCaO/wWIk/rQ
|
|
10
|
+
L/TjY0M70gse8rlBkbo2a8rKhu69RQTRsoaf4DVhDPEeSeI5jVrRDGAMGL3cGuyY
|
|
11
|
+
6CLKGdjVEM78g3JfYOvDU/RvfqD7L89TZ3iN94jrmWdGz34JNlEI5hqK8dd7C5EF
|
|
12
|
+
BEbZ5jgB8s8ReQV8H+MkuffjdAj3ajDDX3DOJMIut1lBrUVD1AaSrGCKHooWoL2e
|
|
13
|
+
twIDAQAB
|
|
14
|
+
-----END PUBLIC KEY-----
|
|
15
|
+
`.trim();
|
|
16
|
+
|
|
17
|
+
export async function verifyKickWebhookSignature(params: {
|
|
18
|
+
messageId: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
rawBody: string;
|
|
21
|
+
signature: string;
|
|
22
|
+
}): Promise<boolean> {
|
|
23
|
+
const { messageId, timestamp, rawBody, signature } = params;
|
|
24
|
+
const payload = `${messageId}.${timestamp}.${rawBody}`;
|
|
25
|
+
|
|
26
|
+
const pemContents = KICK_PUBLIC_KEY_PEM.replace(
|
|
27
|
+
"-----BEGIN PUBLIC KEY-----",
|
|
28
|
+
"",
|
|
29
|
+
)
|
|
30
|
+
.replace("-----END PUBLIC KEY-----", "")
|
|
31
|
+
.replace(/\s+/g, "");
|
|
32
|
+
|
|
33
|
+
const binaryKey = Buffer.from(pemContents, "base64");
|
|
34
|
+
|
|
35
|
+
const publicKey = await crypto.subtle.importKey(
|
|
36
|
+
"spki",
|
|
37
|
+
binaryKey,
|
|
38
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
39
|
+
false,
|
|
40
|
+
["verify"],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const encoder = new TextEncoder();
|
|
44
|
+
const data = encoder.encode(payload);
|
|
45
|
+
const sigBuffer = Buffer.from(signature, "base64");
|
|
46
|
+
|
|
47
|
+
return await crypto.subtle.verify(
|
|
48
|
+
"RSASSA-PKCS1-v1_5",
|
|
49
|
+
publicKey,
|
|
50
|
+
sigBuffer,
|
|
51
|
+
data,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Handler = (event: any, headers: Headers) => void | Promise<void>;
|
|
56
|
+
|
|
57
|
+
export class WebhookRouter {
|
|
58
|
+
private handlers = new Map<string, Set<Handler>>();
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly secret: string,
|
|
62
|
+
private readonly rest: any,
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
on(eventType: string, handler: Handler) {
|
|
66
|
+
if (!this.handlers.has(eventType)) {
|
|
67
|
+
this.handlers.set(eventType, new Set());
|
|
68
|
+
}
|
|
69
|
+
this.handlers.get(eventType)!.add(handler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async handleRequest(params: {
|
|
73
|
+
rawBody: string;
|
|
74
|
+
headers: Record<string, string | string[] | undefined>;
|
|
75
|
+
}) {
|
|
76
|
+
const headers = params.headers;
|
|
77
|
+
|
|
78
|
+
const messageId = headers["kick-event-message-id"] as string;
|
|
79
|
+
const timestamp = headers["kick-event-message-timestamp"] as string;
|
|
80
|
+
const signature = headers["kick-event-signature"] as string;
|
|
81
|
+
const eventType = headers["kick-event-type"] as string;
|
|
82
|
+
|
|
83
|
+
if (!messageId || !timestamp || !signature || !eventType) {
|
|
84
|
+
throw new Error("Missing required Kick webhook headers");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const valid = await verifyKickWebhookSignature({
|
|
88
|
+
messageId,
|
|
89
|
+
timestamp,
|
|
90
|
+
rawBody: params.rawBody,
|
|
91
|
+
signature,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!valid) {
|
|
95
|
+
throw new Error("Invalid Kick webhook signature");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const payload = JSON.parse(params.rawBody);
|
|
99
|
+
|
|
100
|
+
const handlers = this.handlers.get(eventType);
|
|
101
|
+
if (!handlers) return;
|
|
102
|
+
|
|
103
|
+
for (const handler of handlers) {
|
|
104
|
+
await handler(payload, headers as any);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
createServer(options: CreateServerOptions) {
|
|
109
|
+
return webhookServer(this, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async ngrok(options?: {
|
|
113
|
+
port?: number;
|
|
114
|
+
path?: string;
|
|
115
|
+
authtoken?: string;
|
|
116
|
+
domain?: string;
|
|
117
|
+
region?: string;
|
|
118
|
+
onUrl?: (url: string) => void;
|
|
119
|
+
}) {
|
|
120
|
+
return await ngrokAdapter(options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async subscribe(options: { events: { name: string; version?: number }[] }) {
|
|
124
|
+
await this.rest.fetch("/public/v1/events/subscriptions", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
events: options.events.map((e) => ({
|
|
128
|
+
name: e.name,
|
|
129
|
+
version: e.version ?? 1,
|
|
130
|
+
})),
|
|
131
|
+
method: "webhook",
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WebhookRouter } from "./WebhookRouter";
|
|
2
|
+
import type { CreateServerOptions } from "../../types";
|
|
3
|
+
import { logger } from "../Logger.ts";
|
|
4
|
+
|
|
5
|
+
export function webhookServer(
|
|
6
|
+
router: WebhookRouter,
|
|
7
|
+
options: CreateServerOptions = {},
|
|
8
|
+
): Bun.Server<undefined> {
|
|
9
|
+
const port = options.port ?? 3000;
|
|
10
|
+
const path = options.path ?? "/kick/webhook";
|
|
11
|
+
|
|
12
|
+
const server = Bun.serve({
|
|
13
|
+
port,
|
|
14
|
+
async fetch(req) {
|
|
15
|
+
const url = new URL(req.url);
|
|
16
|
+
|
|
17
|
+
if (req.method !== "POST" || url.pathname !== path) {
|
|
18
|
+
return new Response(null, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const rawBody = await req.text();
|
|
23
|
+
const headers = Object.fromEntries(req.headers.entries());
|
|
24
|
+
|
|
25
|
+
await router.handleRequest({
|
|
26
|
+
rawBody,
|
|
27
|
+
headers,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return new Response("OK", { status: 200 });
|
|
31
|
+
} catch (err) {
|
|
32
|
+
logger.error(err);
|
|
33
|
+
return new Response("Unauthorized", { status: 401 });
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
logger.success("Webhook server started", { port, path });
|
|
39
|
+
|
|
40
|
+
return server;
|
|
41
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|
package/types/api.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type GetCategoriesParams
|
|
3
|
+
* @property {number?} [cursor] - The cursor for pagination. (Minimum: 4, Maximum: 28)
|
|
4
|
+
* @property {number?} [limit] - The maximum number of categories to return. (Minimum: 1, Maximum: 1000)
|
|
5
|
+
* @property {string[]?} [name] - Category names.
|
|
6
|
+
* @property {string[]?} [tags] - Tags associated with the categories.
|
|
7
|
+
* @property {number[]?} [id] - Category IDs.
|
|
8
|
+
*/
|
|
9
|
+
export type GetCategoriesParams = {
|
|
10
|
+
cursor?: number;
|
|
11
|
+
limit?: number;
|
|
12
|
+
name?: string[];
|
|
13
|
+
tags?: string[];
|
|
14
|
+
id?: number[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @interface KickChannel
|
|
19
|
+
* @property {string} slug - The unique identifier for the channel.
|
|
20
|
+
* @property {string} stream_title - The title of the current stream.
|
|
21
|
+
* @property {object} [category] - The category of the stream, if available.
|
|
22
|
+
* @property {number} category.id - The unique identifier for the category.
|
|
23
|
+
* @property {string} category.name - The name of the category.
|
|
24
|
+
*/
|
|
25
|
+
export interface KickChannel {
|
|
26
|
+
slug: string;
|
|
27
|
+
stream_title: string;
|
|
28
|
+
category?: {
|
|
29
|
+
id: number;
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ChannelRewardBase {
|
|
35
|
+
background_color?: string;
|
|
36
|
+
cost: number;
|
|
37
|
+
description: string;
|
|
38
|
+
is_enabled?: boolean;
|
|
39
|
+
is_user_input_required?: boolean;
|
|
40
|
+
should_redemptions_skip_request_queue?: boolean;
|
|
41
|
+
title: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface KickChannelReward extends ChannelRewardBase {
|
|
45
|
+
id: string;
|
|
46
|
+
is_paused: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GetChannelRewardsResponse {
|
|
50
|
+
data: KickChannelReward[];
|
|
51
|
+
message: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GetRedemptionsParams {
|
|
55
|
+
reward_id?: string;
|
|
56
|
+
status?: "pending" | "fulfilled" | "canceled";
|
|
57
|
+
id?: string[];
|
|
58
|
+
cursor?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface KickRedemption {
|
|
62
|
+
id: string;
|
|
63
|
+
redeemed_at: string;
|
|
64
|
+
redeemer: {
|
|
65
|
+
user_id: number;
|
|
66
|
+
};
|
|
67
|
+
status: string;
|
|
68
|
+
user_input?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface RedemptionData {
|
|
72
|
+
redemptions: KickRedemption[];
|
|
73
|
+
reward: {
|
|
74
|
+
can_manage: boolean;
|
|
75
|
+
cost: number;
|
|
76
|
+
description: string;
|
|
77
|
+
id: string;
|
|
78
|
+
is_deleted: boolean;
|
|
79
|
+
title: string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface GetRedemptionsResponse {
|
|
84
|
+
data: RedemptionData[];
|
|
85
|
+
message: string;
|
|
86
|
+
pagination: {
|
|
87
|
+
next_cursor: string | null;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface RedemptionActionResponse {
|
|
92
|
+
data: { id: string; reason: string }[];
|
|
93
|
+
message: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type KickOkResponse = {
|
|
97
|
+
data: Record<string, never> | {};
|
|
98
|
+
message: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type ModerationBanRequest = {
|
|
102
|
+
broadcaster_user_id: number;
|
|
103
|
+
user_id: number;
|
|
104
|
+
reason?: string;
|
|
105
|
+
duration?: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type ModerationUnbanRequest = {
|
|
109
|
+
broadcaster_user_id: number;
|
|
110
|
+
user_id: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type KickLivestream = {
|
|
114
|
+
broadcaster_user_id: number;
|
|
115
|
+
channel_id: number;
|
|
116
|
+
slug: string;
|
|
117
|
+
stream_title: string;
|
|
118
|
+
viewer_count: number;
|
|
119
|
+
language: string;
|
|
120
|
+
has_mature_content: boolean;
|
|
121
|
+
started_at: string;
|
|
122
|
+
thumbnail: string;
|
|
123
|
+
profile_picture: string;
|
|
124
|
+
custom_tags: string[];
|
|
125
|
+
category: {
|
|
126
|
+
id: number;
|
|
127
|
+
name: string;
|
|
128
|
+
thumbnail: string;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type KickLivestreamsResponse = {
|
|
133
|
+
data: KickLivestream[];
|
|
134
|
+
message: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type KickLivestreamStatsResponse = {
|
|
138
|
+
data: {
|
|
139
|
+
total_count: number;
|
|
140
|
+
};
|
|
141
|
+
message: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type KickLeaderboardEntry = {
|
|
145
|
+
gifted_amount: number;
|
|
146
|
+
rank: number;
|
|
147
|
+
user_id: number;
|
|
148
|
+
username: string;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type KickLeaderboardResponse = {
|
|
152
|
+
data: {
|
|
153
|
+
lifetime: KickLeaderboardEntry[];
|
|
154
|
+
month: KickLeaderboardEntry[];
|
|
155
|
+
week: KickLeaderboardEntry[];
|
|
156
|
+
};
|
|
157
|
+
message: string;
|
|
158
|
+
};
|
package/types/auth.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the response received when getting or refreshing tokens from Kick.
|
|
3
|
+
*
|
|
4
|
+
* - `access_token`: The access token used for authentication.
|
|
5
|
+
* - `refresh_token`: The refresh token used to obtain new access tokens.
|
|
6
|
+
* - `token_type`: (Optional) The type of the token, typically "Bearer".
|
|
7
|
+
* - `expires_in`: (Optional) The duration in seconds until the access token expires.
|
|
8
|
+
* - `scope`: (Optional) The scopes granted to the access token.
|
|
9
|
+
*
|
|
10
|
+
* @interface KickTokenResponse
|
|
11
|
+
* @property {string} access_token - The access token used for authentication.
|
|
12
|
+
* @property {string} refresh_token - The refresh token used to obtain new access tokens.
|
|
13
|
+
* @property {string} [token_type] - (Optional) The type of the token, typically "Bearer".
|
|
14
|
+
* @property {number} [expires_in] - (Optional) The duration in seconds until the access token expires.
|
|
15
|
+
* @property {string} [scope] - (Optional) The scopes granted to the access token.
|
|
16
|
+
*/
|
|
17
|
+
export interface KickTokenResponse {
|
|
18
|
+
access_token: string;
|
|
19
|
+
refresh_token: string;
|
|
20
|
+
token_type?: string;
|
|
21
|
+
expires_in?: number;
|
|
22
|
+
scope?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for Kick authentication including initial tokens and token update callback.
|
|
27
|
+
*
|
|
28
|
+
* - `initialTokens`: (Optional) Initial tokens to set up the authentication.
|
|
29
|
+
* - `onTokenUpdate`: (Optional) Callback function invoked when tokens are updated.
|
|
30
|
+
*
|
|
31
|
+
* @interface KickAuthOptions
|
|
32
|
+
* @property {KickTokenResponse} [initialTokens] - (Optional) Initial tokens to set up the authentication.
|
|
33
|
+
* @property {(tokens: KickTokenResponse) => void | Promise<void>} [onTokenUpdate] - (Optional) Callback function invoked when tokens are updated.
|
|
34
|
+
*/
|
|
35
|
+
export interface KickAuthOptions {
|
|
36
|
+
initialTokens?: KickTokenResponse;
|
|
37
|
+
onTokenUpdate?: (tokens: KickTokenResponse) => void | Promise<void>;
|
|
38
|
+
}
|
package/types/chat.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a message in a chat interface.
|
|
3
|
+
* - `content`: The text content of the message.
|
|
4
|
+
* - `type`: The type of message, either from a "bot" or a "user". Defaults to "bot" if not specified.
|
|
5
|
+
*
|
|
6
|
+
* @interface ChatMessage
|
|
7
|
+
* @property {string} content - The text content of the message.
|
|
8
|
+
* @property {"bot" | "user"} [type] - The type of message, either "bot" or "user".
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export interface ChatMessage {
|
|
12
|
+
content: string;
|
|
13
|
+
type?: "bot" | "user";
|
|
14
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { KickAuthOptions } from "./auth";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Available OAuth2 scopes for Kick API.
|
|
5
|
+
* @type {string}
|
|
6
|
+
*/
|
|
7
|
+
type KickScopes =
|
|
8
|
+
| "user:read"
|
|
9
|
+
| "channel:read"
|
|
10
|
+
| "channel:write"
|
|
11
|
+
| "channel:rewards:read"
|
|
12
|
+
| "channel:rewards:write"
|
|
13
|
+
| "chat:write"
|
|
14
|
+
| "streamkey:read"
|
|
15
|
+
| "events:subscribe"
|
|
16
|
+
| "moderation:ban"
|
|
17
|
+
| "moderation:chat_message:manage"
|
|
18
|
+
| "kicks:read";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for initializing the KickClient.
|
|
22
|
+
*
|
|
23
|
+
* - `clientId`: The client ID of your Kick application.
|
|
24
|
+
* - `clientSecret`: The client secret of your Kick application.
|
|
25
|
+
* - `redirectUri`: The redirect URI for OAuth2 authorization.
|
|
26
|
+
* - `scopes`: The scopes required for your application.
|
|
27
|
+
* - `state`: Optional state parameter for OAuth2 authorization.
|
|
28
|
+
* - `auth`: Authentication options including token management.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const client = new KickClient({
|
|
33
|
+
* clientId: "your-client-id",
|
|
34
|
+
* clientSecret: "your-client-secret",
|
|
35
|
+
* redirectUri: "http://localhost:3000/callback",
|
|
36
|
+
* scopes: ["chat:read", "chat:write"],
|
|
37
|
+
* auth: {
|
|
38
|
+
* initialTokens: {
|
|
39
|
+
* access_token: "initial-access-token",
|
|
40
|
+
* refresh_token: "initial-refresh-token",
|
|
41
|
+
* expires_in: 7200,
|
|
42
|
+
* scope: "chat:read chat:write",
|
|
43
|
+
* token_type: "Bearer",
|
|
44
|
+
* },
|
|
45
|
+
* },
|
|
46
|
+
* onTokenUpdate: (tokens) => {
|
|
47
|
+
* console.log("Tokens updated:", tokens);
|
|
48
|
+
* }
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @interface KickClientOptions
|
|
53
|
+
* @property {string} clientId - The client ID of your Kick application.
|
|
54
|
+
* @property {string} clientSecret - The client secret of your Kick application.
|
|
55
|
+
* @property {string} redirectUri - The redirect URI for OAuth2 authorization.
|
|
56
|
+
* @property {KickScopes[]} scopes - The scopes required for your application.
|
|
57
|
+
* @property {string} [state] - Optional state parameter for OAuth2 authorization.
|
|
58
|
+
* @property {KickAuthOptions} [auth] - Authentication options including token management.
|
|
59
|
+
*/
|
|
60
|
+
export interface KickClientOptions {
|
|
61
|
+
clientId: string;
|
|
62
|
+
clientSecret: string;
|
|
63
|
+
redirectUri: string;
|
|
64
|
+
scopes: KickScopes[];
|
|
65
|
+
state?: string;
|
|
66
|
+
auth?: KickAuthOptions;
|
|
67
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface KickWebhookEvent<T = any> {
|
|
2
|
+
event: string;
|
|
3
|
+
data: T;
|
|
4
|
+
created_at: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ChatMessageEvent {
|
|
8
|
+
message_id: string;
|
|
9
|
+
replies_to: string | null;
|
|
10
|
+
broadcaster: {
|
|
11
|
+
is_anonymous: boolean;
|
|
12
|
+
user_id: number;
|
|
13
|
+
username: string;
|
|
14
|
+
is_verified: boolean;
|
|
15
|
+
profile_picture: string;
|
|
16
|
+
channel_slug: string;
|
|
17
|
+
identity: any | null;
|
|
18
|
+
};
|
|
19
|
+
sender: {
|
|
20
|
+
is_anonymous: boolean;
|
|
21
|
+
user_id: number;
|
|
22
|
+
username: string;
|
|
23
|
+
is_verified: boolean;
|
|
24
|
+
profile_picture: string;
|
|
25
|
+
channel_slug: string;
|
|
26
|
+
identity: any | null;
|
|
27
|
+
};
|
|
28
|
+
content: string;
|
|
29
|
+
emotes: any[];
|
|
30
|
+
created_at: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CreateServerOptions {
|
|
33
|
+
port?: number;
|
|
34
|
+
path?: string;
|
|
35
|
+
}
|