Skip to content

Commit

Permalink
Replace twurple with http endpoint / raw requests (#145)
Browse files Browse the repository at this point in the history
* initial Axios port trying fetch native next with http endpoint

* Converted to HTTPEndpoint

* better createEndpoint

* moar extends

* Fixed some minor syntax things

* Added more endpoints for twitch api

* Added URLSearchParams handling into helix.ts fetchFn

* Ported all prev implemented nodes to non twurple.

* Added check for expired token before sending helix requests.

* nicer api yay

---------

Co-authored-by: Brendan Allan <brendonovich@outlook.com>
  • Loading branch information
jdudetv and Brendonovich authored Jul 4, 2023
1 parent 1f4a0b8 commit 962ea7e
Show file tree
Hide file tree
Showing 8 changed files with 1,222 additions and 703 deletions.
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@tauri-apps/api": "^1.2.0",
"clsx": "^1.2.1",
"solid-icons": "^1.0.4",
"solid-js": "^1.7.0",
"solid-js": "^1.7.5",
"zod": "^3.21.4"
},
"devDependencies": {
Expand Down
83 changes: 40 additions & 43 deletions packages/package.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,42 @@
{
"name": "@macrograph/packages",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"files": [
"dist/**"
],
"scripts": {
"test": "jest",
"lint": "eslint src",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"typecheck": "tsc -b",
"build": "tsc"
},
"dependencies": {
"@rspc/client": "0.0.0-main-6ed8cc98",
"@rspc/tauri": "0.0.0-main-6ed8cc98",
"@solid-primitives/map": "^0.4.3",
"@solid-primitives/set": "^0.4.4",
"@tauri-apps/api": "^1.3.0",
"@twurple/api": "^6.2.0",
"@twurple/auth": "^6.2.0",
"@twurple/chat": "^6.2.0",
"@twurple/eventsub-ws": "^6.2.0",
"discord.js": "^14.9.0",
"obs-websocket-js": "^5.0.2",
"tmi.js": "^1.8.5",
"typescript-result-option": "^0.2.5",
"zod": "^3.21.4"
},
"devDependencies": {
"@macrograph/core": "workspace:*",
"@total-typescript/ts-reset": "^0.4.2",
"@types/tmi.js": "^1.8.3",
"solid-js": "^1.7.0",
"type-fest": "^2.19.0",
"typescript": "^4.8.4"
},
"peerDependencies": {
"@macrograph/core": "workspace:*",
"solid-js": "^1.7.0"
}
"name": "@macrograph/packages",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"files": [
"dist/**"
],
"scripts": {
"test": "jest",
"lint": "eslint src",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"typecheck": "tsc -b",
"build": "tsc"
},
"dependencies": {
"@rspc/client": "0.0.0-main-6ed8cc98",
"@rspc/tauri": "0.0.0-main-6ed8cc98",
"@solid-primitives/map": "^0.4.3",
"@solid-primitives/set": "^0.4.4",
"@tauri-apps/api": "^1.3.0",
"@twurple/auth": "^6.2.0",
"discord.js": "^14.9.0",
"obs-websocket-js": "^5.0.2",
"tmi.js": "^1.8.5",
"typescript-result-option": "^0.2.5",
"zod": "^3.21.4"
},
"devDependencies": {
"@macrograph/core": "workspace:*",
"@total-typescript/ts-reset": "^0.4.2",
"@types/tmi.js": "^1.8.3",
"solid-js": "^1.7.0",
"type-fest": "^2.19.0",
"typescript": "^4.8.4"
},
"peerDependencies": {
"@macrograph/core": "workspace:*",
"solid-js": "^1.7.0"
}
}
27 changes: 11 additions & 16 deletions packages/src/discord/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,23 @@ import pkg from "./pkg";
import { botToken, setBotToken } from "./auth";
import { createResource, createRoot } from "solid-js";
import { GUILD_MEMBER_SCHEMA, ROLE_SCHEMA, USER_SCHEMA } from "./schemas";
import { createEndpoint } from "../httpEndpoint";
import { Maybe, Option, rspcClient, t } from "@macrograph/core";
import { createEndpoint, nativeFetch } from "../httpEndpoint";
import { Maybe, rspcClient, t } from "@macrograph/core";

const root = createEndpoint({
path: "https://discord.com/api/v10",
fetchFn: async (args) => {
fetchFn: async (url, args) => {
const token = botToken();
if (token === null) throw new Error("No bot token!");

const { data } = await rspcClient.query([
"http.json",
{
...args,
headers: {
...args?.headers,
"Content-Type": "application/json",
Authorization: `Bot ${token}`,
},
return nativeFetch(url, {
...args,
headers: {
...args?.headers,
"Content-Type": "application/json",
Authorization: `Bot ${token}`,
},
]);

return data;
});
},
});

Expand Down Expand Up @@ -93,7 +88,7 @@ pkg.createNonEventSchema({
},
async run({ ctx }) {
await api.channels(ctx.getInput("channelId")).messages.post(z.any(), {
body: { Json: { content: ctx.getInput("message") } },
body: { content: ctx.getInput("message") },
});
},
});
Expand Down
57 changes: 49 additions & 8 deletions packages/src/httpEndpoint.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,58 @@
import { rspcClient, HTTPRequest, HTTPBody } from "@macrograph/core";
import { rspcClient, HTTPRequest, HTTPMethod } from "@macrograph/core";
import { z } from "zod";
import * as core from "@macrograph/core";

type Endpoint = ReturnType<typeof createEndpoint>;
type HTTPMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

export const nativeFetch = (req: HTTPRequest) =>
rspcClient.query(["http.json", req]).then((d) => d.data);
type HTTPBody = FormData | URLSearchParams | object;
type HTTPData = {
method: HTTPMethod;
headers?: HTTPRequest["headers"];
body?: FormData | URLSearchParams | object;
};

export const nativeFetch = async (url: string, data: HTTPData) => {
let body: core.HTTPBody | undefined;

if (data.body instanceof URLSearchParams) {
url = `${url}?${data.body.toString()}`;
} else {
body =
data.body instanceof FormData
? {
Form: [...data.body.entries()].reduce(
(acc, curr) => ({
...acc,
[curr[0]]: curr[1] as string,
}),
{} as Extract<core.HTTPBody, { Form: any }>["Form"]
),
}
: { Json: data.body };
}

const d = await rspcClient.query([
"http.json",
{
url,
...data,
body,
},
]);

return d.data;
};

interface EndpointArgs {
path: string;
extend?: Endpoint;
fetchFn?: (req: HTTPRequest) => any;
fetchFn?: (url: string, req: HTTPData) => any;
}

export function createEndpoint({ path, extend, fetchFn }: EndpointArgs) {
if (extend) path = `${extend.path}${path}`;

const resolvedFetchFn: (req: HTTPRequest) => Promise<any> =
const resolvedFetchFn: (url: string, req: HTTPData) => any =
fetchFn ?? extend?.fetchFn ?? nativeFetch;

const createFetcher =
Expand All @@ -25,8 +61,7 @@ export function createEndpoint({ path, extend, fetchFn }: EndpointArgs) {
schema: TSchema,
args?: { body?: HTTPBody }
): Promise<z.infer<TSchema>> => {
const res = await resolvedFetchFn({
url: path,
const res = await resolvedFetchFn(path, {
method,
...args,
});
Expand All @@ -37,6 +72,12 @@ export function createEndpoint({ path, extend, fetchFn }: EndpointArgs) {
return {
path,
fetchFn: resolvedFetchFn,
extend(path: string) {
return createEndpoint({
path,
extend: this,
});
},
get: createFetcher("GET"),
post: createFetcher("POST"),
put: createFetcher("PUT"),
Expand Down
50 changes: 32 additions & 18 deletions packages/src/twitch/auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { AccessTokenWithUserId, AuthProvider } from "@twurple/auth";
import { Maybe, None, Some } from "@macrograph/core";
import { extractUserId, UserIdResolvable } from "@twurple/api";
import { z } from "zod";
import { ReactiveMap } from "@solid-primitives/map";

const clientId = "ldbp0fkq9yalf2lzsi146i0cip8y59";

export const TWITCH_ACCCESS_TOKEN = "TwitchAccessToken";

export interface AccessTokenWithUsernameAndId extends AccessTokenWithUserId {
export interface User {
userName: string;
userId: string;
accessToken: string;
refreshToken: string;
scope: string[];
expiresIn: number;
obtainmentTimestamp: number;
}

class MacroGraphAuthProvider implements AuthProvider {
tokens: ReactiveMap<string, AccessTokenWithUsernameAndId>;
export interface UserIdResolvableType {
/**
* The ID of the user.
*/
id: string;
}

export type UserIdResolvable = string | number | UserIdResolvableType;

class MacroGraphAuthProvider {
tokens: ReactiveMap<string, User>;

constructor(public clientId: string) {
this.tokens = Maybe(localStorage.getItem(TWITCH_ACCCESS_TOKEN))
Expand All @@ -26,11 +39,6 @@ class MacroGraphAuthProvider implements AuthProvider {
.unwrapOr(new ReactiveMap());
}

getCurrentScopesForUser(userId: UserIdResolvable) {
const id = extractUserId(userId);
return this.tokens.get(id)?.scope ?? [];
}

logOut(userID: UserIdResolvable) {
const id = extractUserId(userID);
this.tokens.delete(id);
Expand All @@ -51,7 +59,7 @@ class MacroGraphAuthProvider implements AuthProvider {
};
}

async addUser(token: AccessTokenWithUsernameAndId) {
async addUser(token: User) {
const res = await fetch("https://api.twitch.tv/helix/users", {
method: "GET",
headers: {
Expand All @@ -70,9 +78,7 @@ class MacroGraphAuthProvider implements AuthProvider {
return userId;
}

async getAnyAccessToken(
userId?: UserIdResolvable
): Promise<AccessTokenWithUsernameAndId> {
async getAnyAccessToken(userId?: UserIdResolvable): Promise<User> {
return {
...Maybe(
this.tokens.get(
Expand All @@ -84,10 +90,8 @@ class MacroGraphAuthProvider implements AuthProvider {
};
}

async refreshAccessTokenForUser(
user: UserIdResolvable
): Promise<AccessTokenWithUsernameAndId> {
const userId = extractUserId(user);
async refreshAccessTokenForUser(user: string): Promise<User> {
const userId = user;

const { userName, refreshToken } = Maybe(this.tokens.get(userId)).expect(
"refreshAccessTokenForUser missing token"
Expand Down Expand Up @@ -146,4 +150,14 @@ const SCHEMA = z.record(
})
);

export function extractUserId(user: UserIdResolvable): string {
if (typeof user === "string") {
return user;
} else if (typeof user === "number") {
return user.toString(10);
} else {
return user.id;
}
}

export const auth = new MacroGraphAuthProvider(clientId);
30 changes: 16 additions & 14 deletions packages/src/twitch/eventsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "solid-js";
import { t } from "@macrograph/core";
import { auth } from "./auth";
import { z } from "zod";

const SubTypes = [
"channel.update",
Expand Down Expand Up @@ -71,20 +72,21 @@ const { state } = createRoot(() => {

await Promise.all(
SubTypes.map((type) =>
helix.client.eventSub.createSubscription(
type,
type == "channel.follow" ? "2" : "1",
{
from_broadcaster_user_id: userId,
broadcaster_user_id: userId,
moderator_user_id: userId,
helix.client.eventsub.subscriptions.post(z.any(), {
body: {
type,
version: type == "channel.follow" ? "2" : "1",
condition: {
broadcaster_user_id: userId,
moderator_user_id: userId,
to_broadcaster_user_id: userId,
},
transport: {
method: "websocket",
session_id: info.payload.session.id,
},
},
{
method: "websocket",
session_id: info.payload.session.id,
},
{ id: userId }
)
})
)
);

Expand Down Expand Up @@ -704,7 +706,7 @@ pkg.createEventSchema({
});
io.dataOutput({
id: "rewardId",
name: "Rewards Id",
name: "Reward Id",
type: t.string(),
});
io.dataOutput({
Expand Down
Loading

0 comments on commit 962ea7e

Please sign in to comment.