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,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
...tseslint.configs.stylisticTypeChecked,
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: true,
|
projectService: true,
|
||||||
tsconfigRootDir: import.meta.dirname
|
tsconfigRootDir: import.meta.dirname,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
import {type Handler} from "@netlify/functions";
|
import {type Handler} from "@netlify/functions";
|
||||||
|
import { Gitlab } from "@gitbeaker/rest";
|
||||||
import {type GitlabInfo} from "../../src/components/Info/Coding/GitLab.js";
|
import {type GitlabInfo} from "../../src/components/Info/Coding/GitLab.js";
|
||||||
|
|
||||||
const handler: Handler = async () => {
|
const handler: Handler = async () => {
|
||||||
const gitlab = await fetch("https://gitlab.com/api/v4/events?action=pushed", {
|
const api = new Gitlab({token: process.env.API_GITLAB!});
|
||||||
method: "GET",
|
const gitlab = await api.Events.all({action: "pushed"});
|
||||||
headers: {
|
|
||||||
"PRIVATE-TOKEN": process.env.API_GITLAB!,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {created_at} = (await gitlab.json() as Record<string, any>)[0];
|
const created_at = gitlab.at(0)?.created_at;
|
||||||
if (typeof created_at !== "string") {
|
if (typeof created_at !== "string") {
|
||||||
return {
|
return {
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
|
|
|
@ -1,29 +1,38 @@
|
||||||
import {type Handler} from "@netlify/functions";
|
import {type Handler} from "@netlify/functions";
|
||||||
import { KitsuclubInfo } from "../../src/components/Info/Fediverse/KitsuClub.js";
|
import { KitsuclubInfo } from "../../src/components/Info/Fediverse/KitsuClub.js";
|
||||||
|
import { api } from "./shared/api.js";
|
||||||
|
|
||||||
const handler: Handler = async () => {
|
const handler: Handler = async () => {
|
||||||
const kitsuclub = await fetch("https://kitsunes.club/api/users/notes", {
|
const kitsuclub = await api<{
|
||||||
method: "POST",
|
user: {
|
||||||
headers: {
|
name: string
|
||||||
"Authorization": `Bearer ${process.env.API_KITSUCLUB}`,
|
username: string
|
||||||
"Content-Type": "application/json",
|
avatarUrl: string
|
||||||
},
|
emojis: Record<string, string>
|
||||||
body: JSON.stringify({
|
}
|
||||||
"userId": "a2hgd7delf",
|
text: string
|
||||||
"limit": 1,
|
createdAt: string
|
||||||
"withReplies": false,
|
}[]>("https://kitsunes.club/api/users/notes", process.env.API_KITSUCLUB, true, JSON.stringify({
|
||||||
"withRepliesToSelf": false,
|
"userId": "a2hgd7delf",
|
||||||
"withQuotes": true,
|
"limit": 1,
|
||||||
"withRenotes": false,
|
"withReplies": false,
|
||||||
"withBots": true,
|
"withRepliesToSelf": false,
|
||||||
"withNonPublic": true,
|
"withQuotes": true,
|
||||||
"withChannelNotes": false,
|
"withRenotes": false,
|
||||||
"withFiles": false,
|
"withBots": true,
|
||||||
"allowPartial": false,
|
"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 = {
|
const activity: KitsuclubInfo = {
|
||||||
id: details.user.username,
|
id: details.user.username,
|
||||||
username: details.user.name,
|
username: details.user.name,
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
export async function api<T>(url: string, restful_token?: string): Promise<T> {
|
export async function api<T>(url: string, restful_token?: string, post?: boolean, body?: BodyInit): Promise<T> {
|
||||||
|
let fetched: Promise<Response>;
|
||||||
return (restful_token ? fetch(url, {headers: {"Authorization": `Bearer ${restful_token}`}}) : fetch(url))
|
if (post) {
|
||||||
.then(async response => {
|
fetched = fetch(url, {
|
||||||
if (!response.ok) {
|
method: "POST",
|
||||||
console.error(response.status, response.statusText);
|
headers: {
|
||||||
throw new Error("Request failed :(");
|
"Authorization": `Bearer ${restful_token}`,
|
||||||
}
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
return response.json() as Promise<T>;
|
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 {api} from "./shared/api.js";
|
||||||
import {type SpeedruncomInfo} from "../../src/components/Info/Speedrunning/Speedruncom.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 () => {
|
const handler: Handler = async () => {
|
||||||
// using the API's embedding would be stupid here, as that'd create lag due to irrelevant runs
|
// using the API's embedding would be stupid here, as that'd create lag due to irrelevant runs
|
||||||
const speedruncom = await api<{
|
const speedruncom = await api<Runs>("https://www.speedrun.com/api/v1/users/j03v45mj/personal-bests");
|
||||||
data: {
|
const data = speedruncom.data.at(0);
|
||||||
place: number;
|
|
||||||
run: {
|
if (!data) {
|
||||||
weblink: string;
|
return {
|
||||||
game: string;
|
statusCode: 404,
|
||||||
level: string | undefined;
|
};
|
||||||
category: string | undefined;
|
}
|
||||||
date: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}>("https://www.speedrun.com/api/v1/users/j03v45mj/personal-bests");
|
|
||||||
|
|
||||||
const detailsToRequest = [new Promise((resolve) => {
|
const detailsToRequest = [new Promise((resolve) => {
|
||||||
resolve(api<{
|
resolve(api<Game>(`https://www.speedrun.com/api/v1/games/${data.run.game}`));
|
||||||
data: {
|
|
||||||
names: {
|
|
||||||
international: string;
|
|
||||||
};
|
|
||||||
assets: {
|
|
||||||
"cover-tiny": {
|
|
||||||
uri: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>(`https://www.speedrun.com/api/v1/games/${speedruncom.data[0].run.game}`));
|
|
||||||
})];
|
})];
|
||||||
|
|
||||||
if (speedruncom.data[0].run.level) {
|
if (data.run.level) {
|
||||||
detailsToRequest.push(new Promise((resolve) => {
|
detailsToRequest.push(new Promise((resolve) => {
|
||||||
resolve(api<{
|
resolve(api<Level>(`https://www.speedrun.com/api/v1/levels/${data.run.level}`));
|
||||||
data: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}>(`https://www.speedrun.com/api/v1/levels/${speedruncom.data[0].run.level}`));
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (speedruncom.data[0].run.category) {
|
if (data.run.category) {
|
||||||
detailsToRequest.push(new Promise((resolve) => {
|
detailsToRequest.push(new Promise((resolve) => {
|
||||||
resolve(api<{
|
resolve(api<Level>(`https://www.speedrun.com/api/v1/categories/${data.run.category}`));
|
||||||
data: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}>(`https://www.speedrun.com/api/v1/categories/${speedruncom.data[0].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 = {
|
const run: SpeedruncomInfo = {
|
||||||
place: speedruncom.data[0].place,
|
place: data.place,
|
||||||
link: speedruncom.data[0].run.weblink,
|
link: data.run.weblink,
|
||||||
date: speedruncom.data[0].run.date,
|
date: data.run.date,
|
||||||
thumbnail: details[0].data.assets["cover-tiny"].uri,
|
thumbnail: game.data.assets["cover-tiny"].uri,
|
||||||
game: details[0].data.names.international,
|
game: game.data.names.international,
|
||||||
details: details.slice(1).map((d) => (d.data as {name: string}).name),
|
details: details.map((d) => d.data.name),
|
||||||
|
time: sec2time(data.run.times.primary_t),
|
||||||
|
video: data.run.videos.links.at(0)?.uri,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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};
|
export {handler};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {type Handler} from "@netlify/functions";
|
import {type Handler} from "@netlify/functions";
|
||||||
import {api} from "./shared/api.js";
|
import {api} from "./shared/api.js";
|
||||||
import {type WanikaniInfo} from "../../src/components/Info/Japanese/Wanikani.js";
|
import {type WanikaniInfo} from "../../src/components/Info/Japanese/Wanikani.js";
|
||||||
|
import { WKLevelProgression, WKResetCollection, WKSummary } from "@bachmacintosh/wanikani-api-types";
|
||||||
|
|
||||||
interface Subject {
|
interface Subject {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -49,7 +50,7 @@ function addStuffToLearn(ids: number[], data: {available_at: string; subject_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler: Handler = async () => {
|
const handler: Handler = async () => {
|
||||||
const data: any[] = await Promise.all([
|
const data = await Promise.all([
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
resolve(api("https://api.wanikani.com/v2/level_progressions", process.env.API_WANIKANI));
|
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;
|
total_count: number;
|
||||||
data: {
|
data: WKLevelProgression[];
|
||||||
data: {
|
};
|
||||||
level: number;
|
const resets = data[1] as WKResetCollection;
|
||||||
unlocked_at: undefined | string;
|
const summary = data[2] as WKSummary;
|
||||||
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];
|
|
||||||
|
|
||||||
const subjectIdsLessons: number[] = [];
|
const subjectIdsLessons: number[] = [];
|
||||||
const subjectIdsReviews: number[] = [];
|
|
||||||
for (const lesson of summary.data.lessons) {
|
for (const lesson of summary.data.lessons) {
|
||||||
for (const subjectId of lesson.subject_ids) {
|
for (const subjectId of lesson.subject_ids) {
|
||||||
subjectIdsLessons.push(subjectId);
|
subjectIdsLessons.push(subjectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subjectIdsReviews: number[] = [];
|
||||||
for (const review of summary.data.reviews) {
|
for (const review of summary.data.reviews) {
|
||||||
for (const subjectId of review.subject_ids) {
|
for (const subjectId of review.subject_ids) {
|
||||||
subjectIdsReviews.push(subjectId);
|
subjectIdsReviews.push(subjectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
// next_reviews | Checks what reviews will be available in the next 23 hours
|
// 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
|
// 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
|
const now = new Date();
|
||||||
.map((r: {subject_ids: number[]; available_at: Date | string}) => {
|
const nextReviews = summary.data.reviews.filter((r) => new Date(r.available_at) > now && r.subject_ids.length);
|
||||||
r.available_at = new Date(r.available_at); return r;
|
const moreThingsToReviewAt = nextReviews.at(0)?.available_at ?? summary.data.next_reviews_at;
|
||||||
})
|
|
||||||
.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 subjectIdsAll = subjectIdsLessons.concat(subjectIdsReviews);
|
const subjectIdsAll = subjectIdsLessons.concat(subjectIdsReviews);
|
||||||
const subjects = await api<{data: Subject[]}>(`https://api.wanikani.com/v2/subjects?ids=${subjectIdsAll.toString()}`, process.env.API_WANIKANI);
|
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 ."
|
"lint": "bunx eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bachmacintosh/wanikani-api-types": "^1.7.0",
|
||||||
"@carbon/icons-react": "^11.55.0",
|
"@carbon/icons-react": "^11.55.0",
|
||||||
|
"@gitbeaker/rest": "^42.1.0",
|
||||||
"@netlify/functions": "^2.8.2",
|
"@netlify/functions": "^2.8.2",
|
||||||
"@octokit/rest": "^20.1.1",
|
"@octokit/rest": "^20.1.1",
|
||||||
"mongodb": "^6.13.0",
|
"mongodb": "^6.13.0",
|
||||||
|
|
|
@ -1,28 +1,16 @@
|
||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
import Website from "../../Website.js";
|
import Website from "../../Website.js";
|
||||||
|
import { WKLevelProgression, WKReset } from "@bachmacintosh/wanikani-api-types";
|
||||||
|
|
||||||
export type WanikaniInfo = {
|
export type WanikaniInfo = {
|
||||||
progression: {
|
progression: {
|
||||||
total_count: number;
|
total_count: number;
|
||||||
data: {
|
data: WKLevelProgression[];
|
||||||
data: {
|
|
||||||
level: number;
|
|
||||||
unlocked_at: undefined | string;
|
|
||||||
completed_at: undefined | string;
|
|
||||||
abandoned_at: undefined | string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
resets: {
|
resets: WKReset[];
|
||||||
data: {
|
|
||||||
created_at: string;
|
|
||||||
original_level: number;
|
|
||||||
target_level: number;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
lessons: Item[];
|
lessons: Item[];
|
||||||
reviews: Item[];
|
reviews: Item[];
|
||||||
moreThingsToReviewAt: string | undefined;
|
moreThingsToReviewAt: string | null;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export type SpeedruncomInfo = {
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
game: string;
|
game: string;
|
||||||
details: string[];
|
details: string[];
|
||||||
|
time: string;
|
||||||
|
video?: string;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
|
||||||
export default function Speedruncom() {
|
export default function Speedruncom() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue