Skip to content

Commit

Permalink
feat: v2 functions
Browse files Browse the repository at this point in the history
  • Loading branch information
mnsinri committed Sep 23, 2024
1 parent f315bb2 commit 94159b9
Show file tree
Hide file tree
Showing 21 changed files with 751 additions and 502 deletions.
2 changes: 2 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"ignore": [
"**/node_modules/**",
"**/src/**",
"**/types/**",
"**/arc/**",
".eslintrc.js",
".gitignore",
"tsconfig.dev.json",
Expand Down
4 changes: 3 additions & 1 deletion functions/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ typings/
# Node.js dependency directory
node_modules/

.env
.env*
.secret.local
*.http
*.log
streamerMaster.json
17 changes: 0 additions & 17 deletions functions/env.d.ts

This file was deleted.

52 changes: 52 additions & 0 deletions functions/src/api/baseClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DocumentReference, DocumentData } from "firebase-admin/firestore";
import { Platform, Channel, BaseStream } from "../../types";

export abstract class Client {
private token = "";

constructor(
protected tokenDoc: DocumentReference<DocumentData>,
private platform: Platform,
) {}

protected abstract generateToken(): Promise<string>;
abstract getChannels(userIds: string[]): Promise<Channel[]>;
abstract getStreams(userIds: string[]): Promise<BaseStream[]>;

protected async getToken(): Promise<string> {
if (this.token) return this.token;

const doc = await this.tokenDoc.get();
this.token = doc.data()?.[this.platform];

if (!this.token) this.token = await this.generateToken();

return this.token;
}

protected async setToken(token: string): Promise<void> {
this.token = token;
await this.tokenDoc.update({ [this.platform]: token });
}

protected async request(
createRequest: (token: string) => Request,
): Promise<any> {
const token = await this.getToken();

const req = createRequest(token);
const response = await fetch(req);

if (response.ok) return response.json();

if (response.status === 401) {
const newToken = await this.generateToken();
const secondResponse = await fetch(createRequest(newToken));
return secondResponse.json();
}

throw new Error(
`request failed.\n${response.url}\n${response.status}:${response.statusText}`,
);
}
}
4 changes: 4 additions & 0 deletions functions/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./baseClient";
export * from "./youtube";
export * from "./twitch";
export * from "./twitCasting";
137 changes: 137 additions & 0 deletions functions/src/api/twitCasting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { DocumentReference, DocumentData } from "firebase-admin/firestore";
import { Client } from "./baseClient";
import { BaseStream, Channel, Config } from "../../types";
import { calcTTL } from "../utils";

export class TwitCastingClient extends Client {
private clientId: string;
private clientCode: string;
private clientSecret: string;

constructor(
tokenDoc: DocumentReference<DocumentData>,
config: Config["twitCasting"],
) {
super(tokenDoc, "twitCasting");
this.clientId = config.clientId.value();
this.clientCode = config.clientCode.value();
this.clientSecret = config.clientSecret.value();
}

// codeの更新がCFからできないので、機能しない
protected override async generateToken(): Promise<string> {
const params = new URLSearchParams({
code: this.clientCode,
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: "authorization_code",
redirect_uri: "https://vspo-stream-schedule.web.app/",
});
const request = new Request(
"https://apiv2.twitcasting.tv/oauth2/access_token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
},
);

const response = await fetch(request);

if (!response.ok)
throw new Error(
`generateToken request failed. ${response.status}:${response.statusText}`,
);

const { ["access_token"]: token } = await response.json();

await this.setToken(token);
return token;
}

override async getChannels(userIds: string[]): Promise<Channel[]> {
if (!userIds.length) return [];

const token = await this.getToken();

const requests = userIds.map((id) => {
const createRequest = (token: string) => {
return new Request(`https://apiv2.twitcasting.tv/users/${id}`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"X-Api-Version": "2.0",
},
});
};

// return this.request(createRequest);

return fetch(createRequest(token));
});

const responses = await Promise.all(requests);

if (responses.some((r) => !r.ok)) return [];

const bodies = await Promise.all(responses.map((r) => r.json()));

return bodies.map((v) => ({
id: v.user.screen_id,
name: v.user.name,
icon: v.user.image,
platform: "twitCasting",
}));
}

override async getStreams(userIds: string[]): Promise<BaseStream[]> {
if (!userIds.length) return [];

const token = await this.getToken();

const requests = userIds.map((id) => {
const createRequest = (token: string) => {
return new Request(
`https://apiv2.twitcasting.tv/users/${id}/current_live`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"X-Api-Version": "2.0",
},
},
);
};

// return this.request(createRequest);

return fetch(createRequest(token));
});

const responses = await Promise.all(requests);

if (responses.some((r) => !r.ok)) return [];

const bodies = await Promise.all(responses.map((r) => r.json()));

return bodies.map((v) => {
const startTime = new Date(v.movie.created * 1000).toISOString();

return {
id: v.movie.id,
channelId: v.movie.user_id,
title: v.movie.title,
thumbnail: v.movie.large_thumbnail,
url: v.movie.link,
scheduledStartTime: startTime,
startTime,
platform: "twitCasting",
ttl: calcTTL(startTime, 7),
};
});
}
}
135 changes: 135 additions & 0 deletions functions/src/api/twitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { DocumentReference, DocumentData } from "firebase-admin/firestore";
import { Client } from "./baseClient";
import { BaseStream, Channel, Config } from "../../types";
import { calcTTL } from "../utils";

export class TwitchClient extends Client {
private clientId: string;
private clientSecret: string;

constructor(
tokenDoc: DocumentReference<DocumentData>,
config: Config["twitch"],
) {
super(tokenDoc, "twitch");
this.clientId = config.clientId.value();
this.clientSecret = config.clientSecret.value();
}

setThumbnailSize(url: string) {
return url.replace(/%?{width}/, "320").replace(/%?{height}/, "180");
}

protected override async generateToken(): Promise<string> {
const query = new URLSearchParams({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: "client_credentials",
});
const request = new Request(`https://id.twitch.tv/oauth2/token?${query}`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});

const response = await fetch(request);

if (!response.ok)
throw new Error(
`generateToken request failed. ${response.status}:${response.statusText}`,
);

const { ["access_token"]: token } = await response.json();

await this.setToken(token);
return token;
}

override async getChannels(userIds: string[]): Promise<Channel[]> {
if (!userIds.length) return [];

const createRequest = (token: string) => {
const query = new URLSearchParams(userIds.map((id) => ["id", id]));
return new Request(`https://api.twitch.tv/helix/users?${query}`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Client-Id": this.clientId,
"Content-Type": "application/json",
},
});
};

const bodies = await this.request(createRequest);

return bodies.data.map((v: any) => ({
id: v.id,
name: v.display_name,
icon: v.profile_image_url,
platform: "twitch",
}));
}

override async getStreams(userIds: string[]): Promise<BaseStream[]> {
if (!userIds.length) return [];

const createRequest = (token: string) => {
const query = new URLSearchParams([
["first", `${userIds.length}`],
...userIds.map((id) => ["user_id", id]),
]);
return new Request(`https://api.twitch.tv/helix/streams?${query}`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Client-Id": this.clientId,
"Content-Type": "application/json",
},
});
};

const body = await this.request(createRequest);

return body.data.map((v: any) => ({
id: v.id,
channelId: v.user_id,
title: v.title,
thumbnail: this.setThumbnailSize(v.thumbnail_url),
url: `https://www.twitch.tv/${v.user_login}`,
scheduledStartTime: v.started_at,
startTime: v.started_at,
platform: "twitch",
ttl: calcTTL(v.started_at, 7),
}));
}

async updateStreamToVideo<T extends BaseStream>(stream: T): Promise<T> {
const createRequest = (token: string) => {
const query = new URLSearchParams([
["user_id", stream.channelId],
["type", "archive"],
["first", "1"],
]);
return new Request(`https://api.twitch.tv/helix/videos?${query}`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Client-Id": this.clientId,
"Content-Type": "application/json",
},
});
};

const result = await this.request(createRequest);
const video = result.data.shift();

if (stream.id !== video.stream_id) throw new Error("can not updated.");

return {
...stream,
url: video.url,
thumbnail: this.setThumbnailSize(video.thumbnail_url),
};
}
}
Loading

0 comments on commit 94159b9

Please sign in to comment.