Skip to content

Commit

Permalink
feat: eventsub context and hook
Browse files Browse the repository at this point in the history
  • Loading branch information
6lr61 committed Sep 11, 2024
1 parent caa9456 commit df085f0
Show file tree
Hide file tree
Showing 15 changed files with 599 additions and 5 deletions.
14 changes: 10 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { useContext } from "react";
import "./App.css";
import { AuthStateContext } from "./contexts/auth-state/AuthStateContext";
import LoginButton from "./components/LoginButton";
import "./App.css";
import { useEventSub } from "./hooks/useEventSub";

export default function App() {
const context = useContext(AuthStateContext);
const authContext = useContext(AuthStateContext);
const { lastMessage } = useEventSub("channel.chat.message", {
broadcaster_user_id: authContext?.authState?.user.id,
user_id: authContext?.authState?.user.id,
});

if (!context) {
if (!authContext) {
return <p>Missing AuthStateContext provider?</p>;
}

return (
<>
<p>Hello: {context.authState?.user.login}</p>
<p>Hello: {authContext.authState?.user.login}</p>
<p>Last message: {JSON.stringify(lastMessage)}</p>
<LoginButton />
</>
);
Expand Down
8 changes: 8 additions & 0 deletions src/contexts/event-sub/EventSubContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from "react";
import type { EventSub } from "../../utils/event-sub/EventSub";

interface Value {
subscribe: typeof EventSub.instance.subscribe;
}

export const EventSubContext = createContext<Value | null>(null);
18 changes: 18 additions & 0 deletions src/contexts/event-sub/EventSubProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EventSub } from "../../utils/event-sub/EventSub";
import { EventSubContext } from "./EventSubContext";

interface Props {
children: React.ReactNode;
}

export default function EventSubProvider({
children,
}: Props): React.ReactElement {
const subscribe = EventSub.instance.subscribe.bind(EventSub.instance);

return (
<EventSubContext.Provider value={{ subscribe }}>
{children}
</EventSubContext.Provider>
);
}
52 changes: 52 additions & 0 deletions src/hooks/useEventSub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { EventSubContext } from "../contexts/event-sub/EventSubContext";
import { AuthStateContext } from "../contexts/auth-state/AuthStateContext";

export function useEventSub(
type: string | string[],
condition: Record<string, unknown>,
bufferSize = 50
) {
const authStateContext = useContext(AuthStateContext);
const eventSubContext = useContext(EventSubContext);
const [messages, setMessages] = useState<Record<string, unknown>[]>([]);
const [lastMessage, setLastMessage] = useState<Record<string, unknown>>();

if (!authStateContext) {
throw new Error("useEventSub: Needs an AuthStateContext Provider!");
}

if (!eventSubContext) {
throw new Error("useEventSub: Needs a EventSubContext Provider!");
}

const { subscribe } = eventSubContext;

const handleMessage = useCallback(
(message: Record<string, unknown>) => {
setMessages((messages) => [...messages.slice(-bufferSize + 1), message]);
setLastMessage(() => message);
},
[bufferSize]
);

useEffect(() => {
const { authState } = authStateContext;

if (!authState) {
return;
}

if (Object.values(condition).some((value) => value === undefined)) {
console.error(
"useEventSub: Was given a bad subscription condition:",
condition
);
return;
}

return subscribe(authState, type, condition, handleMessage);
}, [authStateContext, condition, handleMessage, subscribe, type]);

return { lastMessage, messages };
}
5 changes: 4 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import AuthStateProvider from "./contexts/auth-state/AuthStateProvider.tsx";
import EventSubProvider from "./contexts/event-sub/EventSubProvider.tsx";

const rootElement = document.querySelector("#root");

Expand All @@ -13,7 +14,9 @@ if (!(rootElement instanceof HTMLDivElement)) {
createRoot(rootElement).render(
<StrictMode>
<AuthStateProvider>
<App />
<EventSubProvider>
<App />
</EventSubProvider>
</AuthStateProvider>
</StrictMode>
);
65 changes: 65 additions & 0 deletions src/utils/api/event-sub/subscribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { AuthState } from "../../../contexts/auth-state/AuthStateContext";

interface SubscriptionsResponse {
data: [
{
id: string;
status: "enabled";
type: string;
version: string;
condition: Record<string, unknown>;
/** RFC3339 Date Time */
created_at: string;
transport: {
method: "websocket";
session_id: string;
};
connected_at: string;
cost: number;
}
];
total: number;
total_cost: number;
max_total_cost: number;
}

export async function subscribe(
authState: AuthState,
sessionId: string,
type: string,
version: string,
condition: Record<string, unknown>
): Promise<SubscriptionsResponse> {
const response = await fetch(
"https://api.twitch.tv/helix/eventsub/subscriptions",
{
method: "POST",
headers: {
Authorization: `Bearer ${authState.token.value}`,
"Client-Id": authState.client.id,
"Content-Type": "application/json",
},
body: JSON.stringify({
type,
version,
condition,
transport: {
method: "websocket",
session_id: sessionId,
},
}),
}
);

if (!response.ok) {
throw new Error(
`subscriptions: Bad HTTP response ${response.status.toString()} ${
response.statusText
}`
);
}

const result = (await response.json()) as SubscriptionsResponse;

return result;
}
25 changes: 25 additions & 0 deletions src/utils/api/event-sub/unsubscribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { AuthState } from "../../../contexts/auth-state/AuthStateContext";

export async function unsubscribe(
authState: AuthState,
subscriptionId: string
): Promise<void> {
const url = new URL("https://api.twitch.tv/helix/eventsub/subscriptions");
url.searchParams.set("id", subscriptionId);

const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${authState.token.value}`,
"Client-Id": authState.client.id,
},
});

if (!response.ok) {
throw new Error(
`subscriptions: Bad HTTP response ${response.status.toString()} ${
response.statusText
}`
);
}
}
Loading

0 comments on commit df085f0

Please sign in to comment.