Merge pull request 'Yandere-API' (#3) from Yandere-API into main

Reviewed-on: https://gitea.fzzin.31718216.xyz/fzzinchemical/HotDog/pulls/3
This commit is contained in:
2025-04-10 14:39:59 +00:00
9 changed files with 227 additions and 209 deletions

View File

@@ -1,5 +1,6 @@
//TODO Add optional extensions like limit at Posts with tags etc. //TODO Add optional extensions like limit at Posts with tags etc.
import * as xml_parser from "jsr:@melvdouc/xml-parser"; import * as xml_parser from "jsr:@melvdouc/xml-parser";
import { requestRaw, requestJSON } from "@root/structures/apiRequest.ts";
export type ImageResponse = { export type ImageResponse = {
preview_url: string; preview_url: string;
@@ -49,22 +50,6 @@ export const postUrl = new URL(`${baseUrl}/index.php?page=dapi&s=post&q=index&js
const tagUrl = `${baseUrl}/index.php?page=dapi&s=tag&q=index`; const tagUrl = `${baseUrl}/index.php?page=dapi&s=tag&q=index`;
const commentsUrl = `${baseUrl}/index.php?page=dapi&s=comment&q=index`; const commentsUrl = `${baseUrl}/index.php?page=dapi&s=comment&q=index`;
export async function requestJSON<T>(url: string) {
const response = await requestRaw(url);
return <T> await response.json();
}
async function requestRaw(url: string) {
const response = await fetch(url, {
headers: { "Accept": "application/json" },
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response;
}
//List
// Comments // Comments
export async function getPostComments(postID: number): Promise<CommentResponse[]> { export async function getPostComments(postID: number): Promise<CommentResponse[]> {
const response = await requestRaw(`${commentsUrl}&post_id=${postID}`); const response = await requestRaw(`${commentsUrl}&post_id=${postID}`);

View File

@@ -1,6 +1,7 @@
import { assert } from "@std/assert/assert"; import { assert } from "@std/assert/assert";
import { ImageResponse, postUrl, requestJSON } from "./api.ts"; import { requestJSON } from "@root/structures/apiRequest.ts";
import { Embed, EmbedAuthor, EmbedImage } from "@root/structures/embeds.ts"; import { Embed, EmbedAuthor, EmbedImage } from "@root/structures/embeds.ts";
import { ImageResponse, postUrl } from "./api.ts";
const keys = ["limit", "id", "pid", "tags"] as const; const keys = ["limit", "id", "pid", "tags"] as const;
type PostKeys = typeof keys[number]; type PostKeys = typeof keys[number];

View File

@@ -1,128 +0,0 @@
import { assert } from "@std/assert/assert";
const apiUrl = "https://yande.re/post.json?api_version=2";
type APIResponse = {
posts: {
id: number;
tags: string;
created_at: string;
creator_id: number;
approver_id: number;
author: string;
change: number;
source: string;
score: number;
md5: string;
file_size: number;
file_ext: string;
file_url: string;
is_shown_in_index: boolean;
preview_url: string;
preview_width: number;
preview_height: number;
actual_preview_width: number;
actual_preview_height: number;
sample_url: string;
sample_width: number;
sample_height: number;
sample_file_size: number;
jpeg_url: string;
jpeg_width: number;
jpeg_height: number;
rating: string;
is_rating_locked: boolean;
has_children: boolean;
parent_id: number;
status: string;
is_pending: boolean;
width: number;
height: number;
is_held: boolean;
frames_pending_string: string;
frames_pending: [];
frames_string: string;
frames: [];
is_note_locked: boolean;
last_noted_at: string;
last_commented_at: string;
}[];
};
let page = 1;
let baseRequest = await fetch(apiUrl, {
headers: { "Accept": "application/json" },
})
.then(async (response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return <APIResponse> await response.json();
});
const hyperlinkarray: string[] = [];
for (const k of baseRequest.posts) {
hyperlinkarray.push(k.file_url);
}
export async function setPage(newpage: number) {
page = newpage;
hyperlinkarray.length = 0;
await refresh();
}
export function getPage() {
return page;
}
//duplicate code, also found in rule34/api.ts
export async function refresh() {
await fetch(`${apiUrl}&page=${page}`, {
headers: { "Accept": "application/json" },
})
.then(async (response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
baseRequest = <APIResponse> await response.json();
for (const k of baseRequest.posts) {
if (!hyperlinkarray.includes(k.file_url)) {
hyperlinkarray.push(k.file_url);
}
}
});
}
export async function fetchNextPage() {
page += 1;
await refresh();
}
export async function dropYandere() {
if (hyperlinkarray.length === 0) {
await fetchNextPage();
return await dropYandere();
}
return hyperlinkarray.pop()!;
}
export async function dropYandere5() {
let tmp = "";
for (let i = 0; i < 5; i++) {
tmp += await dropYandere() + "\n";
}
return tmp;
}
//Test Stack resize when fetching new data
Deno.test("Refresh Stack", async () => {
await refresh();
const previousStack = [...hyperlinkarray];
await new Promise((r) => setTimeout(r, 10000));
await refresh();
assert(
previousStack.length <= hyperlinkarray.length,
"Stack-size did not increase as expected!",
);
});

View File

@@ -0,0 +1,72 @@
import { handlePostRequest } from "@root/plugins/yandere/api/post.ts";
const baseURL = "https://yande.re";
const apikeys = [
"post",
"tags",
"artist",
"comments",
"wiki",
"notes",
"users",
"forum",
"pools",
] as const;
type APIKeys = typeof apikeys[number];
function prepareURLObjectForRequest(type: APIKeys) {
const tmp = new URL(baseURL);
tmp.searchParams.append("api_version", "3");
switch (type) {
case "post":
tmp.pathname = "post.json";
break;
case "tags":
tmp.pathname = "tag.json";
break;
case "artist":
tmp.pathname = "artist.json";
break;
case "comments":
tmp.pathname = "comment";
break;
case "wiki":
tmp.pathname = "wiki.json";
break;
case "notes":
tmp.pathname = "note.json";
break;
case "users":
tmp.pathname = "user.json";
break;
case "forum":
tmp.pathname = "forum.json";
break;
case "pools":
tmp.pathname = "pool.json";
break;
default:
throw Error("unknown ");
}
return tmp;
}
export function getPosts(search? : string){
const url = prepareURLObjectForRequest("post")
return handlePostRequest(url, search)
}
Deno.test("Empty Post Request", async() => {
console.debug(await getPosts())
})
Deno.test("Post Request with tag", async() => {
console.debug(await getPosts("[tags:love_live!_(series)]"))
})
Deno.test("Post Request with limit", async() => {
console.debug(await getPosts("[limit: 1)]"))
})
Deno.test("Post Request with page", async() => {
console.debug(await getPosts("[page: 30)]"))
})

View File

@@ -0,0 +1,118 @@
import { requestJSON } from "@root/structures/apiRequest.ts";
import { DiscordEmbed } from "npm:discordeno@18.0.1";
type PostResponse = {
id: number;
tags: string;
created_at: string;
creator_id: number;
approver_id: number;
author: string;
change: number;
source: string;
score: number;
md5: string;
file_size: number;
file_ext: string;
file_url: string;
is_shown_in_index: boolean;
preview_url: string;
preview_width: number;
preview_height: number;
actual_preview_width: number;
actual_preview_height: number;
sample_url: string;
sample_width: number;
sample_height: number;
sample_file_size: number;
jpeg_url: string;
jpeg_width: number;
jpeg_height: number;
rating: string;
is_rating_locked: boolean;
has_children: boolean;
parent_id: number;
status: string;
is_pending: boolean;
width: number;
height: number;
is_held: boolean;
frames_pending_string: string;
frames_pending: [];
frames_string: string;
frames: [];
is_note_locked: boolean;
last_noted_at: string;
last_commented_at: string;
}[];
const postSearchkeys = ["limit", "page", "tags"];
function parsePostListArgs(url: URL, args: string) {
// const urlCopy: URL = copyObject<URL>(url);
const argarr = args.replaceAll(/(\[|\ |\])/g, "").split(",");
for (const arg of argarr) {
const [k, v] = arg.split(":");
if (url === undefined) throw Error("undefined Object: url!")
if (k === undefined || v === undefined) {
throw Error(
`undefined key or value in ${parsePostListArgs.name}, got k:${k}, v:${v}`,
);
}
if (postSearchkeys.includes(k)) {
console.debug(k, v)
console.debug(JSON.stringify(url))
url.searchParams.append(k, v);
} else {
throw Error(
`unknown parameter was given in ${parsePostListArgs.name}, got k:${k}, v:${v}`,
);
}
}
return url;
}
async function returnDiscordEmbeds(
url: URL,
): Promise<DiscordEmbed[]> {
const response: PostResponse = await requestJSON<PostResponse>(
url.toString(),
);
const embeds: DiscordEmbed[] = [];
for (const post of response) {
embeds.push(
{
url: post.source,
title: post.id.toString(),
author: {
name: post.author,
},
image: {
url: post.file_url,
width: post.width,
height: post.height,
},
},
);
}
return embeds;
}
// function copyObject<T>(obj: T) {
// const newObject: T = Object.
/**
* Post-Request Handler Function, that returns DiscordEmbeds.
* @param url URL Object
* @param search Search string formatted like the following: [tags: something, page: 1, limit: 666], some parameters can miss.
*/
export async function handlePostRequest(url: URL, search?: string): Promise<DiscordEmbed[]> {
// const urlCopy = copyObject<URL>(url);
//parse
if (search) {
return await returnDiscordEmbeds(parsePostListArgs(url, search));
} else {
return await returnDiscordEmbeds(url);
}
}

View File

@@ -1,69 +1,25 @@
import { Bot, Message } from "npm:discordeno@18.0.1"; import { Bot, Embed, Message } from "npm:discordeno@18.0.1";
import { dropYandere, dropYandere5, getPage, refresh, setPage } from "./api.ts";
import { defaultString } from "@root/defaultString.ts"; import { defaultString } from "@root/defaultString.ts";
import { logMessage } from "@root/logging.ts"; import { logMessage } from "@root/logging.ts";
import { getPosts } from "@root/plugins/yandere/api/api.ts";
import { debug } from "node:console";
export async function yandereMessageHandler(bot: Bot, message: Message) { export async function yandereMessageHandler(bot: Bot, message: Message) {
const command = message.content.split(" ").slice(1).join(" "); const command = message.content.split(" ").slice(1).join(" ");
const args = message.content.split(" ").slice(1); const args = command.match(/(\[\.+\])/g)
if (command === "drop") {
switch (command) { const embed = await getPosts("[limit: 1]")
case `drop`: bot.helpers.sendMessage(message.channelId, {
embeds: [embed as Embed]
})
} else if (command.startsWith("drop [") && command.endsWith("]")) {
logMessage(message); logMessage(message);
if ( if (args === null) throw Error("args is null")
message.channelId === 754338073101205524n || const generatedEmbeds = await getPosts(args[0]);
message.guildId === undefined for (const embed of generatedEmbeds) {
) {
await refresh();
bot.helpers.sendMessage(message.channelId, { bot.helpers.sendMessage(message.channelId, {
content: defaultString(await dropYandere()), embeds: [embed as Embed],
});
}
break;
case `drop 5`:
logMessage(message);
if (
message.channelId === 754338073101205524n ||
message.guildId === undefined
) {
await refresh();
bot.helpers.sendMessage(message.channelId, {
content: defaultString(await dropYandere5()),
});
}
break;
case `page`:
logMessage(message);
if (
message.channelId === 754338073101205524n ||
message.guildId === undefined
) {
if (args[0] === undefined) {
bot.helpers.sendMessage(message.channelId, {
content: "Please provide a page number",
});
} else if (isNaN(parseInt(args[0]))) {
bot.helpers.sendMessage(message.channelId, {
content: "Please provide a valid number",
});
} else {
await setPage(parseInt(args[0]));
bot.helpers.sendMessage(message.channelId, {
content: "Page set to " + args[0],
}); });
} }
} }
break;
case `getpage`:
logMessage(message);
if (
message.channelId === 754338073101205524n ||
message.guildId === undefined
) {
bot.helpers.sendMessage(message.channelId, {
content: "Page is " + getPage(),
});
}
break;
}
} }

View File

View File

@@ -4,7 +4,7 @@ import {
getPage, getPage,
refresh, refresh,
setPage, setPage,
} from "@root/plugins/yandere/api.ts"; } from "./api/api.ts";
import { assertEquals } from "@std/assert/equals"; import { assertEquals } from "@std/assert/equals";
//tests //tests
/* /*

View File

@@ -0,0 +1,14 @@
export async function requestJSON<T>(url: string) {
const response = await requestRaw(url);
return <T> await response.json();
}
export async function requestRaw(url: string) {
const response = await fetch(url, {
headers: { "Accept": "application/json" },
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response;
}