Type some Netlify functions more strongly
So no more `any`, and use some typing from some libraries
This commit is contained in:
parent
96911a8d95
commit
41d33ab964
10 changed files with 158 additions and 144 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -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,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -9,6 +9,8 @@ export type SpeedruncomInfo = {
|
|||
thumbnail: string;
|
||||
game: string;
|
||||
details: string[];
|
||||
time: string;
|
||||
video?: string;
|
||||
} | undefined;
|
||||
|
||||
export default function Speedruncom() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue