Type some Netlify functions more strongly

So no more `any`, and use some typing from some libraries
This commit is contained in:
Taevas 2025-02-17 13:26:10 +01:00
parent 96911a8d95
commit 41d33ab964
10 changed files with 158 additions and 144 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -8,11 +8,18 @@ export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylisticTypeChecked,
{
settings: {
react: {
version: "detect",
}
}
},
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
tsconfigRootDir: import.meta.dirname,
}
}
},

View file

@ -1,17 +1,12 @@
import {type Handler} from "@netlify/functions";
import { Gitlab } from "@gitbeaker/rest";
import {type GitlabInfo} from "../../src/components/Info/Coding/GitLab.js";
const handler: Handler = async () => {
const gitlab = await fetch("https://gitlab.com/api/v4/events?action=pushed", {
method: "GET",
headers: {
"PRIVATE-TOKEN": process.env.API_GITLAB!,
"Content-Type": "application/json",
"Accept": "application/json",
},
});
const api = new Gitlab({token: process.env.API_GITLAB!});
const gitlab = await api.Events.all({action: "pushed"});
const {created_at} = (await gitlab.json() as Record<string, any>)[0];
const created_at = gitlab.at(0)?.created_at;
if (typeof created_at !== "string") {
return {
statusCode: 404,

View file

@ -1,29 +1,38 @@
import {type Handler} from "@netlify/functions";
import { KitsuclubInfo } from "../../src/components/Info/Fediverse/KitsuClub.js";
import { api } from "./shared/api.js";
const handler: Handler = async () => {
const kitsuclub = await fetch("https://kitsunes.club/api/users/notes", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.API_KITSUCLUB}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"userId": "a2hgd7delf",
"limit": 1,
"withReplies": false,
"withRepliesToSelf": false,
"withQuotes": true,
"withRenotes": false,
"withBots": true,
"withNonPublic": true,
"withChannelNotes": false,
"withFiles": false,
"allowPartial": false,
})
});
const kitsuclub = await api<{
user: {
name: string
username: string
avatarUrl: string
emojis: Record<string, string>
}
text: string
createdAt: string
}[]>("https://kitsunes.club/api/users/notes", process.env.API_KITSUCLUB, true, JSON.stringify({
"userId": "a2hgd7delf",
"limit": 1,
"withReplies": false,
"withRepliesToSelf": false,
"withQuotes": true,
"withRenotes": false,
"withBots": true,
"withNonPublic": true,
"withChannelNotes": false,
"withFiles": false,
"allowPartial": false,
}));
const details = kitsuclub.at(0);
if (!details) {
return {
statusCode: 404,
};
}
const details = (await kitsuclub.json() as Record<string, any>)[0];
const activity: KitsuclubInfo = {
id: details.user.username,
username: details.user.name,

View file

@ -1,12 +1,24 @@
export async function api<T>(url: string, restful_token?: string): Promise<T> {
return (restful_token ? fetch(url, {headers: {"Authorization": `Bearer ${restful_token}`}}) : fetch(url))
.then(async response => {
if (!response.ok) {
console.error(response.status, response.statusText);
throw new Error("Request failed :(");
}
return response.json() as Promise<T>;
export async function api<T>(url: string, restful_token?: string, post?: boolean, body?: BodyInit): Promise<T> {
let fetched: Promise<Response>;
if (post) {
fetched = fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${restful_token}`,
"Content-Type": "application/json",
},
body,
});
} else {
fetched = (restful_token ? fetch(url, {headers: {"Authorization": `Bearer ${restful_token}`}}) : fetch(url));
}
return fetched.then(async response => {
if (!response.ok) {
console.error(response.status, response.statusText);
throw new Error("Request failed :(");
}
return response.json() as Promise<T>;
});
}

View file

@ -2,65 +2,86 @@ import {type Handler} from "@netlify/functions";
import {api} from "./shared/api.js";
import {type SpeedruncomInfo} from "../../src/components/Info/Speedrunning/Speedruncom.js";
interface Runs {
data: {
place: number;
run: {
weblink: string;
game: string;
level?: string;
category?: string;
videos: {
links: {
uri: string
}[]
}
date: string;
times: {
primary_t: number
}
};
}[]
}
interface Game {
data: {
names: {
international: string;
};
assets: {
"cover-tiny": {
uri: string;
};
};
};
}
interface Level {
data: {
name: string;
};
}
const handler: Handler = async () => {
// using the API's embedding would be stupid here, as that'd create lag due to irrelevant runs
const speedruncom = await api<{
data: {
place: number;
run: {
weblink: string;
game: string;
level: string | undefined;
category: string | undefined;
date: string;
};
}[];
}>("https://www.speedrun.com/api/v1/users/j03v45mj/personal-bests");
const speedruncom = await api<Runs>("https://www.speedrun.com/api/v1/users/j03v45mj/personal-bests");
const data = speedruncom.data.at(0);
if (!data) {
return {
statusCode: 404,
};
}
const detailsToRequest = [new Promise((resolve) => {
resolve(api<{
data: {
names: {
international: string;
};
assets: {
"cover-tiny": {
uri: string;
};
};
};
}>(`https://www.speedrun.com/api/v1/games/${speedruncom.data[0].run.game}`));
resolve(api<Game>(`https://www.speedrun.com/api/v1/games/${data.run.game}`));
})];
if (speedruncom.data[0].run.level) {
if (data.run.level) {
detailsToRequest.push(new Promise((resolve) => {
resolve(api<{
data: {
name: string;
};
}>(`https://www.speedrun.com/api/v1/levels/${speedruncom.data[0].run.level}`));
resolve(api<Level>(`https://www.speedrun.com/api/v1/levels/${data.run.level}`));
}));
}
if (speedruncom.data[0].run.category) {
if (data.run.category) {
detailsToRequest.push(new Promise((resolve) => {
resolve(api<{
data: {
name: string;
};
}>(`https://www.speedrun.com/api/v1/categories/${speedruncom.data[0].run.category}`));
resolve(api<Level>(`https://www.speedrun.com/api/v1/categories/${data.run.category}`));
}));
}
const details = await Promise.all(detailsToRequest) as [Record<string, any>];
const requests = await Promise.all(detailsToRequest);
const game = requests[0] as Game;
const details = requests.slice(1) as Level[];
const run: SpeedruncomInfo = {
place: speedruncom.data[0].place,
link: speedruncom.data[0].run.weblink,
date: speedruncom.data[0].run.date,
thumbnail: details[0].data.assets["cover-tiny"].uri,
game: details[0].data.names.international,
details: details.slice(1).map((d) => (d.data as {name: string}).name),
place: data.place,
link: data.run.weblink,
date: data.run.date,
thumbnail: game.data.assets["cover-tiny"].uri,
game: game.data.names.international,
details: details.map((d) => d.data.name),
time: sec2time(data.run.times.primary_t),
video: data.run.videos.links.at(0)?.uri,
};
return {
@ -69,4 +90,15 @@ const handler: Handler = async () => {
};
};
// https://gist.github.com/vankasteelj/74ab7793133f4b257ea3
function sec2time(timeInSeconds: number) {
const pad = (num: number, size: number) => ("000" + num).slice(size * -1);
const time = Number(parseFloat(timeInSeconds.toString()).toFixed(3));
const hours = Math.floor(time / 60 / 60);
const minutes = Math.floor(time / 60) % 60;
const seconds = Math.floor(time - minutes * 60);
const milliseconds = Number(time.toString().slice(-3));
return pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "." + pad(milliseconds, 3);
};
export {handler};

View file

@ -1,6 +1,7 @@
import {type Handler} from "@netlify/functions";
import {api} from "./shared/api.js";
import {type WanikaniInfo} from "../../src/components/Info/Japanese/Wanikani.js";
import { WKLevelProgression, WKResetCollection, WKSummary } from "@bachmacintosh/wanikani-api-types";
interface Subject {
id: number;
@ -49,7 +50,7 @@ function addStuffToLearn(ids: number[], data: {available_at: string; subject_ids
}
const handler: Handler = async () => {
const data: any[] = await Promise.all([
const data = await Promise.all([
new Promise((resolve) => {
resolve(api("https://api.wanikani.com/v2/level_progressions", process.env.API_WANIKANI));
}),
@ -61,66 +62,32 @@ const handler: Handler = async () => {
}),
]);
const progression: {
const progression = data[0] as {
total_count: number;
data: {
data: {
level: number;
unlocked_at: undefined | string;
completed_at: undefined | string;
abandoned_at: undefined | string;
};
}[];
} = data[0];
const resets: {
data: [{
data: {
created_at: string;
original_level: number;
target_level: number;
};
}];
} = data[1];
const summary: {
data: {
lessons: {
available_at: string;
subject_ids: number[];
}[];
reviews: {
available_at: string;
subject_ids: number[];
}[];
next_reviews_at: undefined | string;
};
} = data[2];
data: WKLevelProgression[];
};
const resets = data[1] as WKResetCollection;
const summary = data[2] as WKSummary;
const subjectIdsLessons: number[] = [];
const subjectIdsReviews: number[] = [];
for (const lesson of summary.data.lessons) {
for (const subjectId of lesson.subject_ids) {
subjectIdsLessons.push(subjectId);
}
}
const subjectIdsReviews: number[] = [];
for (const review of summary.data.reviews) {
for (const subjectId of review.subject_ids) {
subjectIdsReviews.push(subjectId);
}
}
const now = new Date();
// next_reviews | Checks what reviews will be available in the next 23 hours
// summary.data.next_reviews_at | Checks beyond that, but will be the current time if a review is already available
const nextReviews = summary.data.reviews
.map((r: {subject_ids: number[]; available_at: Date | string}) => {
r.available_at = new Date(r.available_at); return r;
})
.filter((r) => r.available_at > now && r.subject_ids.length) as {subject_ids: number[]; available_at: Date}[];
const moreThingsToReviewAt = nextReviews.at(0)?.available_at.toISOString() ?? summary.data.next_reviews_at;
const now = new Date();
const nextReviews = summary.data.reviews.filter((r) => new Date(r.available_at) > now && r.subject_ids.length);
const moreThingsToReviewAt = nextReviews.at(0)?.available_at ?? summary.data.next_reviews_at;
const subjectIdsAll = subjectIdsLessons.concat(subjectIdsReviews);
const subjects = await api<{data: Subject[]}>(`https://api.wanikani.com/v2/subjects?ids=${subjectIdsAll.toString()}`, process.env.API_WANIKANI);

View file

@ -6,7 +6,9 @@
"lint": "bunx eslint ."
},
"dependencies": {
"@bachmacintosh/wanikani-api-types": "^1.7.0",
"@carbon/icons-react": "^11.55.0",
"@gitbeaker/rest": "^42.1.0",
"@netlify/functions": "^2.8.2",
"@octokit/rest": "^20.1.1",
"mongodb": "^6.13.0",

View file

@ -1,28 +1,16 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
import { WKLevelProgression, WKReset } from "@bachmacintosh/wanikani-api-types";
export type WanikaniInfo = {
progression: {
total_count: number;
data: {
data: {
level: number;
unlocked_at: undefined | string;
completed_at: undefined | string;
abandoned_at: undefined | string;
};
}[];
data: WKLevelProgression[];
};
resets: {
data: {
created_at: string;
original_level: number;
target_level: number;
};
}[];
resets: WKReset[];
lessons: Item[];
reviews: Item[];
moreThingsToReviewAt: string | undefined;
moreThingsToReviewAt: string | null;
} | undefined;
interface Item {

View file

@ -9,6 +9,8 @@ export type SpeedruncomInfo = {
thumbnail: string;
game: string;
details: string[];
time: string;
video?: string;
} | undefined;
export default function Speedruncom() {