feat: scalar openapi stuff

This commit is contained in:
2025-09-01 01:06:36 +02:00
parent e616ac20d4
commit 4c415dacc4
13 changed files with 2037 additions and 39 deletions

View File

@@ -0,0 +1,35 @@
{
"openapi": "3.0.0",
"info": {
"title": "hctv public API",
"version": "0.0.1",
"description": "OpenAPI specification for hackclub.tv's publicly documented API endpoints."
},
"servers": [
{
"url": "http://localhost:3000/api",
"description": "Local development server"
},
{
"url": "https://hctv.srizan.dev/api",
"description": "Production server"
}
],
"components": {
"securitySchemes": {
"auth_session": {
"type": "apiKey",
"in": "cookie",
"name": "auth_session"
}
}
},
"defaultResponseSet": "common",
"apiDir": "./src/app/(ui)/(protected)/api",
"schemaDir": "./src",
"docsUrl": "api-docs",
"ui": "scalar",
"outputFile": "openapi.json",
"includeOpenApiRoutes": false,
"debug": false
}

View File

@@ -11,7 +11,8 @@
"start": "next start",
"lint": "next lint",
"ui:add": "shadcn add",
"check-types": "tsc --noEmit"
"check-types": "tsc --noEmit",
"openapi": "next-openapi-gen"
},
"dependencies": {
"@hctv/auth": "*",
@@ -33,10 +34,12 @@
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@scalar/api-reference-react": "^0.7.42",
"@sentry/nextjs": "^10",
"@slack/web-api": "^7.9.1",
"@uidotdev/usehooks": "^2.4.1",
"@uploadthing/react": "^7.3.1",
"ajv": "^8.17.1",
"arctic": "^3.7.0",
"bullmq": "^5.45.2",
"cheerio": "^1.0.0",
@@ -84,6 +87,7 @@
"@types/ws": "^8.18.0",
"eslint": "^8",
"eslint-config-next": "15.1.3",
"next-openapi-gen": "^0.7.3",
"postcss": "^8",
"shadcn": "^2.7.0",
"tailwindcss": "^3.4.1",

View File

@@ -0,0 +1,466 @@
{
"openapi": "3.0.0",
"info": {
"title": "hctv public API",
"version": "0.0.1",
"description": "OpenAPI specification for hackclub.tv's publicly documented API endpoints."
},
"servers": [
{
"url": "http://localhost:3000/api",
"description": "Local development server"
},
{
"url": "https://hctv.srizan.dev/api",
"description": "Production server"
}
],
"components": {
"securitySchemes": {
"auth_session": {
"type": "apiKey",
"in": "cookie",
"name": "auth_session"
}
},
"schemas": {
"CommitResponse": {
"type": "object",
"properties": {
"version": {
"type": "string",
"nullable": true
},
"commit": {
"type": "string",
"nullable": true
}
}
},
"HLSPathParams": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"HLSResponse": {
"oneOf": [
{},
{
"type": "string"
}
]
},
"ArrayBuffer": {},
"PublishForm": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": false
}
}
},
"StreamKeyRequest": {
"type": "object",
"properties": {
"channel": {
"type": "string",
"nullable": false
}
}
},
"StreamKeyResponse": {
"type": "object",
"properties": {
"key": {
"type": "string"
}
}
},
"FollowParams": {
"type": "object",
"properties": {
"username": {
"type": "string"
}
}
},
"FollowResponse": {
"type": "object",
"properties": {
"following": {
"type": "boolean"
}
}
},
"FollowersParams": {
"type": "object",
"properties": {
"channel": {
"type": "string"
}
}
},
"FollowersResponse": {
"type": "object",
"properties": {
"count": {
"type": "number"
},
"success": {
"type": "boolean"
}
}
},
"StreamInfoResponse": {
"type": "array",
"items": {
"type": "object"
}
},
"StreamInfo": {},
"Channel": {},
"ThumbPathParams": {
"type": "object",
"properties": {
"username": {
"type": "string"
}
}
},
"ThumbResponse": {
"oneOf": [
{},
{
"type": "string"
}
]
},
"Buffer": {}
},
"responses": {}
},
"paths": {
"/commit": {
"get": {
"operationId": "get-commit",
"summary": "Get version.",
"description": "Returns the current version and commit hash of the application.",
"tags": [
"Commit"
],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"version": {
"type": "string",
"nullable": true
},
"commit": {
"type": "string",
"nullable": true
}
}
}
}
}
}
}
}
},
"/rtmp/publish": {
"post": {
"operationId": "post-rtmp-publish",
"summary": "Verifies a stream key",
"description": "Verifies a stream key and redirects to the channel name if valid.",
"tags": [
"Rtmp"
],
"parameters": [],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": false
}
}
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/rtmp/streamKey": {
"post": {
"operationId": "post-rtmp-streamKey",
"summary": "Generate a new stream key",
"description": "Generates (or regenerates) a stream key for a given channel. Requires authentication",
"tags": [
"Rtmp"
],
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"channel": {
"type": "string",
"nullable": false
}
}
}
}
}
},
"responses": {
"201": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"key": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/rtmp/hls/{path}": {
"get": {
"operationId": "get-rtmp-hls-{path}",
"summary": "Serve HLS segments",
"description": "Serves HLS segments from /dev/shm/hls. Requires a valid auth_session cookie.",
"tags": [
"Rtmp"
],
"parameters": [
{
"in": "path",
"name": "path",
"schema": {
"type": "string"
},
"required": true,
"example": "example"
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"oneOf": [
{},
{
"type": "string"
}
]
}
}
}
}
}
}
},
"/stream/follow": {
"get": {
"operationId": "get-stream-follow",
"summary": "Follow or unfollow a channel",
"description": "Follow or unfollow a channel. Requires authentication.",
"tags": [
"Stream"
],
"parameters": [
{
"in": "query",
"name": "username",
"schema": {
"type": "string"
},
"required": false
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"following": {
"type": "boolean"
}
}
}
}
}
}
}
},
"post": {
"operationId": "post-stream-follow",
"summary": "",
"description": "",
"tags": [
"Stream"
],
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/stream/info": {
"get": {
"operationId": "get-stream-info",
"summary": "Get stream information",
"description": "Retrieves stream information based on query parameters. Requires authentication for certain queries.",
"tags": [
"Stream"
],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object"
}
}
}
}
}
}
}
},
"/stream/followers/{channel}": {
"get": {
"operationId": "get-stream-followers-{channel}",
"summary": "Get the number of followers for a channel",
"description": "Retrieves the total number of followers for a specified channel.",
"tags": [
"Stream"
],
"parameters": [
{
"in": "path",
"name": "channel",
"schema": {
"type": "string"
},
"required": true,
"example": "example"
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"count": {
"type": "number"
},
"success": {
"type": "boolean"
}
}
}
}
}
}
}
}
},
"/stream/thumb/{username}": {
"get": {
"operationId": "get-stream-thumb-{username}",
"summary": "Serve user thumbnail",
"description": "Serves user thumbnails. Requires authentication.",
"tags": [
"Stream"
],
"parameters": [
{
"in": "path",
"name": "username",
"schema": {
"type": "string"
},
"required": true,
"example": "johndoe"
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"oneOf": [
{},
{
"type": "string"
}
]
}
}
}
}
}
}
}
}
}

View File

@@ -1,3 +1,14 @@
type CommitResponse = {
version: string | undefined;
commit: string | undefined;
};
/**
* Get version.
* @description Returns the current version and commit hash of the application.
* @response CommitResponse
*/
export function GET() {
return Response.json({
version: process.env.version,

View File

@@ -3,6 +3,18 @@ import fs from 'fs';
import { getRedisConnection } from '@hctv/db';
import { cookies } from 'next/headers';
type HLSPathParams = {
path: string;
};
type HLSResponse = ArrayBuffer | string;
/**
* Serve HLS segments
* @description Serves HLS segments from /dev/shm/hls. Requires a valid auth_session cookie.
* @pathParams HLSPathParams
* @response HLSResponse
* @openapi
*/
export async function GET(request: Request, { params }: { params: Promise<{ path: string }> }) {
const { path } = await params;
const c = await cookies();

View File

@@ -1,9 +1,24 @@
import { prisma } from '@hctv/db';
import { NextRequest } from 'next/server';
type PublishForm = {
name: string;
};
/**
* Verifies a stream key
* @description Verifies a stream key and redirects to the channel name if valid.
* @body PublishForm
* @contentType multipart/form-data
*/
export async function POST(request: NextRequest) {
const formData = await request.formData();
const streamKey = formData.get('name')?.toString() || '';
const streamKey = formData.get('name');
if (typeof streamKey !== 'string') {
return new Response('bad request', {
status: 400,
});
}
const key = await prisma.streamKey.findFirst({
where: {

View File

@@ -2,6 +2,20 @@ import { validateRequest } from '@/lib/auth/validate';
import { prisma } from '@hctv/db';
import { NextRequest } from "next/server";
type StreamKeyRequest = {
channel: string;
};
type StreamKeyResponse = {
key: string;
};
/**
* Generate a new stream key
* @description Generates (or regenerates) a stream key for a given channel. Requires authentication
* @body StreamKeyRequest
* @response StreamKeyResponse
* @openapi
*/
export async function POST(request: NextRequest) {
const { user } = await validateRequest();
const body = await request.json();
@@ -11,6 +25,10 @@ export async function POST(request: NextRequest) {
return new Response('Unauthorized', { status: 401 });
}
if (!channel || typeof channel !== 'string') {
return new Response('Bad Request', { status: 400 });
}
const channelInfo = await prisma.channel.findUnique({
where: { name: channel },
include: {

View File

@@ -3,6 +3,19 @@ import { getNotificationQueue } from '@/lib/workers';
import { prisma } from '@hctv/db';
import { NextRequest } from 'next/server';
type FollowParams = {
username: string;
};
type FollowResponse = {
following: boolean;
};
/**
* Follow or unfollow a channel
* @description Follow or unfollow a channel. Requires authentication.
* @params FollowParams
* @response FollowResponse
* @openapi
*/
export async function GET(request: NextRequest) {
const { user } = await validateRequest();
const searchParams = new URL(request.url).searchParams;

View File

@@ -2,6 +2,20 @@ import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@hctv/db';
import { resolveChannelNameId } from '@/lib/db/resolve';
type FollowersParams = {
channel: string;
};
type FollowersResponse = {
count: number;
success: boolean;
};
/**
* Get the number of followers for a channel
* @description Retrieves the total number of followers for a specified channel.
* @pathParams FollowersParams
* @response FollowersResponse
* @openapi
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ channel: string }> }

View File

@@ -1,9 +1,22 @@
// FIXME: THIS EFFING SUCKS OH MY GOD
import { validateRequest } from '@/lib/auth/validate';
import { Prisma, prisma } from '@hctv/db';
import { Channel, Prisma, prisma, StreamInfo } from '@hctv/db';
import type { NextRequest } from 'next/server';
type StreamInfoResponse = Array<StreamInfo & { channel: Channel }>;
type StreamInfoQuery = {
owned?: boolean;
personal?: boolean;
live?: boolean;
};
/**
* Get stream information
* @description Retrieves stream information based on query parameters. Requires authentication for certain queries.
* @queryParams StreamInfoQuery
* @response StreamInfoResponse
* @openapi
*/
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const shouldGetOwned = searchParams.get('owned') === 'true';

View File

@@ -2,6 +2,18 @@ import { validateRequest } from '@/lib/auth/validate';
import fsP from 'fs/promises';
import fs from 'fs';
type ThumbPathParams = {
username: string;
};
type ThumbResponse = Buffer<ArrayBufferLike> | string;
/**
* Serve user thumbnail
* @description Serves user thumbnails. Requires authentication.
* @pathParams ThumbPathParams
* @response ThumbResponse
* @openapi
*/
export async function GET(request: Request, { params }: { params: Promise<{ username: string }> }) {
const { username } = await params;
const { user } = await validateRequest();

View File

@@ -0,0 +1,16 @@
"use client";
import { ApiReferenceReact } from "@scalar/api-reference-react";
import "@scalar/api-reference-react/style.css";
export default function ApiDocsPage() {
return (
<ApiReferenceReact
configuration={{
_integration: "nextjs",
url: "/openapi.json",
}}
/>
);
}

1441
yarn.lock

File diff suppressed because it is too large Load Diff