fmt
This commit is contained in:
parent
1aacac9c41
commit
642e075721
|
@ -38,8 +38,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
PORT: 8081
|
PORT: 8081
|
||||||
BOT_API_URI: "http://bot-api:8080"
|
BOT_API_URI: "http://bot-api:8080"
|
||||||
GELBOORU_IMAGES_PER_REQUEST: 100 # Number of images per request, maximum 100
|
GELBOORU_IMAGES_PER_REQUEST: 100 # Number of images per request, maximum 100
|
||||||
GELBOORU_TAGS: "2girls sleeping" # Tags of the images. The images will have all of these tags
|
GELBOORU_TAGS: "2girls sleeping" # Tags of the images. The images will have all of these tags
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/usr/src/app:ro
|
- ./:/usr/src/app:ro
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ app.use(express.json());
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
app.get("/", (_, res) => {
|
app.get("/", (_, res) => {
|
||||||
const endpoints = listEndpoints(app);
|
const endpoints = listEndpoints(app);
|
||||||
res.json({ endpoints });
|
res.json({ endpoints });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/image", ImageController.get);
|
app.get("/image", ImageController.get);
|
||||||
|
|
|
@ -3,15 +3,15 @@ import logger from "src/logger";
|
||||||
import ImageService from "src/services/ImageService";
|
import ImageService from "src/services/ImageService";
|
||||||
|
|
||||||
class ImageController {
|
class ImageController {
|
||||||
async get(_: Request, res: Response) {
|
async get(_: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const image = await ImageService.get();
|
const image = await ImageService.get();
|
||||||
res.json(image);
|
res.json(image);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(error)
|
logger.error(error);
|
||||||
res.status(500).json({ "error": `Internal server error: ${error}` });
|
res.status(500).json({ error: `Internal server error: ${error}` });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ImageController();
|
export default new ImageController();
|
10
src/index.ts
10
src/index.ts
|
@ -5,12 +5,12 @@ import logger from "src/logger";
|
||||||
const PORT = env.PORT;
|
const PORT = env.PORT;
|
||||||
|
|
||||||
const server = app.listen(PORT, () =>
|
const server = app.listen(PORT, () =>
|
||||||
logger.info(`Express server listening on port ${PORT}`)
|
logger.info(`Express server listening on port ${PORT}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
logger.info('Server closed.');
|
logger.info("Server closed.");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -3,22 +3,20 @@ import logger from "src/logger";
|
||||||
import { BotApiResponse } from "src/types/BotApiResponse";
|
import { BotApiResponse } from "src/types/BotApiResponse";
|
||||||
|
|
||||||
class BotApiService {
|
class BotApiService {
|
||||||
readonly BOT_API_URI = env.BOT_API_URI;
|
readonly BOT_API_URI = env.BOT_API_URI;
|
||||||
|
|
||||||
async getAll(): Promise<BotApiResponse> {
|
|
||||||
const get_url = `${this.BOT_API_URI}/images`;
|
|
||||||
const response: BotApiResponse = await fetch(get_url)
|
|
||||||
.then(async res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`)
|
|
||||||
throw new Error("Error fetching images");
|
|
||||||
} else {
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
}) as BotApiResponse;
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
async getAll(): Promise<BotApiResponse> {
|
||||||
|
const get_url = `${this.BOT_API_URI}/images`;
|
||||||
|
const response: BotApiResponse = (await fetch(get_url).then(async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`);
|
||||||
|
throw new Error("Error fetching images");
|
||||||
|
} else {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
})) as BotApiResponse;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BotApiService();
|
export default new BotApiService();
|
|
@ -4,23 +4,29 @@ import GelbooruApiResponse from "src/types/GelbooruApiResponse";
|
||||||
import GelbooruServiceResponse from "src/types/GelbooruServiceResponse";
|
import GelbooruServiceResponse from "src/types/GelbooruServiceResponse";
|
||||||
|
|
||||||
class GelbooruApiService {
|
class GelbooruApiService {
|
||||||
async get(): Promise<GelbooruServiceResponse> {
|
async get(): Promise<GelbooruServiceResponse> {
|
||||||
const LIMIT = env.GELBOORU_IMAGES_PER_REQUEST || 100;
|
const LIMIT = env.GELBOORU_IMAGES_PER_REQUEST || 100;
|
||||||
const TAGS = encodeURIComponent(env.GELBOORU_TAGS || "");
|
const TAGS = encodeURIComponent(env.GELBOORU_TAGS || "");
|
||||||
const url: string = `https://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=${LIMIT}&json=1&tags=${TAGS}`;
|
const url: string = `https://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=${LIMIT}&json=1&tags=${TAGS}`;
|
||||||
|
|
||||||
const response: GelbooruApiResponse = await fetch(url)
|
const response: GelbooruApiResponse = (await fetch(url).then(
|
||||||
.then(async res => {
|
async (res) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`)
|
logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`);
|
||||||
throw new Error("Error fetching images");
|
throw new Error("Error fetching images");
|
||||||
} else {
|
} else {
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
}) as GelbooruApiResponse;
|
}
|
||||||
|
)) as GelbooruApiResponse;
|
||||||
|
|
||||||
return { posts: response.post.map(post => ({ url: post.file_url, tags: post.tags.split(" ") })) };
|
return {
|
||||||
}
|
posts: response.post.map((post) => ({
|
||||||
|
url: post.file_url,
|
||||||
|
tags: post.tags.split(" "),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new GelbooruApiService();
|
export default new GelbooruApiService();
|
|
@ -5,32 +5,35 @@ import GelbooruApiService from "src/services/GelbooruApiService";
|
||||||
import logger from "src/logger";
|
import logger from "src/logger";
|
||||||
|
|
||||||
class ImageService {
|
class ImageService {
|
||||||
postsQueue: Image[] = [];
|
postsQueue: Image[] = [];
|
||||||
|
|
||||||
async get(): Promise<Image> {
|
async get(): Promise<Image> {
|
||||||
while (this.postsQueue.length === 0) {
|
while (this.postsQueue.length === 0) {
|
||||||
const validPosts = await this.getNewValidImages();
|
const validPosts = await this.getNewValidImages();
|
||||||
this.postsQueue = validPosts;
|
this.postsQueue = validPosts;
|
||||||
logger.info(`Got ${validPosts.length} images from remote`)
|
logger.info(`Got ${validPosts.length} images from remote`);
|
||||||
}
|
|
||||||
return this.postsQueue.pop() as Image;
|
|
||||||
}
|
}
|
||||||
|
return this.postsQueue.pop() as Image;
|
||||||
|
}
|
||||||
|
|
||||||
private async getNewValidImages(): Promise<Image[]> {
|
private async getNewValidImages(): Promise<Image[]> {
|
||||||
const gelbooruResponse: GelbooruServiceResponse = await GelbooruApiService.get();
|
const gelbooruResponse: GelbooruServiceResponse =
|
||||||
const posts = gelbooruResponse.posts;
|
await GelbooruApiService.get();
|
||||||
|
const posts = gelbooruResponse.posts;
|
||||||
|
|
||||||
const botResponse = await BotApiService.getAll();
|
const botResponse = await BotApiService.getAll();
|
||||||
const imagesUrls = botResponse.images.map(image => image.url);
|
const imagesUrls = botResponse.images.map((image) => image.url);
|
||||||
|
|
||||||
const validPosts = Promise.all(posts
|
const validPosts = Promise.all(
|
||||||
.filter(post => !imagesUrls.some(url => url === post.url))
|
posts
|
||||||
.map((post): Image => {
|
.filter((post) => !imagesUrls.some((url) => url === post.url))
|
||||||
return { url: post.url, tags: post.tags };
|
.map((post): Image => {
|
||||||
}));
|
return { url: post.url, tags: post.tags };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return validPosts;
|
return validPosts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ImageService();
|
export default new ImageService();
|
|
@ -1,5 +1,5 @@
|
||||||
import BotImage from "src/types/BotImage";
|
import BotImage from "src/types/BotImage";
|
||||||
|
|
||||||
export interface BotApiResponse {
|
export interface BotApiResponse {
|
||||||
images: BotImage[]
|
images: BotImage[];
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
export default interface BotImage {
|
export default interface BotImage {
|
||||||
_id: string
|
_id: string;
|
||||||
url: string
|
url: string;
|
||||||
status: string
|
status: string;
|
||||||
tags: string[]
|
tags: string[];
|
||||||
__v: number
|
__v: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,42 @@
|
||||||
export default interface GelbooruApiResponse {
|
export default interface GelbooruApiResponse {
|
||||||
"@attributes": Attributes
|
"@attributes": Attributes;
|
||||||
post: Post[]
|
post: Post[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Attributes {
|
export interface Attributes {
|
||||||
limit: number
|
limit: number;
|
||||||
offset: number
|
offset: number;
|
||||||
count: number
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Post {
|
export interface Post {
|
||||||
id: number
|
id: number;
|
||||||
created_at: string
|
created_at: string;
|
||||||
score: number
|
score: number;
|
||||||
width: number
|
width: number;
|
||||||
height: number
|
height: number;
|
||||||
md5: string
|
md5: string;
|
||||||
directory: string
|
directory: string;
|
||||||
image: string
|
image: string;
|
||||||
rating: string
|
rating: string;
|
||||||
source: string
|
source: string;
|
||||||
change: number
|
change: number;
|
||||||
owner: string
|
owner: string;
|
||||||
creator_id: number
|
creator_id: number;
|
||||||
parent_id: number
|
parent_id: number;
|
||||||
sample: number
|
sample: number;
|
||||||
preview_height: number
|
preview_height: number;
|
||||||
preview_width: number
|
preview_width: number;
|
||||||
tags: string
|
tags: string;
|
||||||
title: string
|
title: string;
|
||||||
has_notes: string
|
has_notes: string;
|
||||||
has_comments: string
|
has_comments: string;
|
||||||
file_url: string
|
file_url: string;
|
||||||
preview_url: string
|
preview_url: string;
|
||||||
sample_url: string
|
sample_url: string;
|
||||||
sample_height: number
|
sample_height: number;
|
||||||
sample_width: number
|
sample_width: number;
|
||||||
status: string
|
status: string;
|
||||||
post_locked: number
|
post_locked: number;
|
||||||
has_children: string
|
has_children: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default interface GelbooruPost {
|
export default interface GelbooruPost {
|
||||||
url: string;
|
url: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import GelbooruPost from "src/types/GelbooruPost";
|
import GelbooruPost from "src/types/GelbooruPost";
|
||||||
|
|
||||||
export default interface GelbooruServiceResponse {
|
export default interface GelbooruServiceResponse {
|
||||||
posts: GelbooruPost[];
|
posts: GelbooruPost[];
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export default interface Image {
|
export default interface Image {
|
||||||
url: string;
|
url: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
|
@ -6,31 +6,31 @@ import request from "supertest";
|
||||||
const imageServiceOriginal = ImageService;
|
const imageServiceOriginal = ImageService;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
mock.module("src/services/ImageService", () => ({
|
mock.module("src/services/ImageService", () => ({
|
||||||
default: imageServiceOriginal,
|
default: imageServiceOriginal,
|
||||||
}));
|
}));
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("endpoint returns the correct status codes", () => {
|
describe("endpoint returns the correct status codes", () => {
|
||||||
it("should return 200 if successful", async () => {
|
it("should return 200 if successful", async () => {
|
||||||
// Can't mock ImageService partially because of bun incomplete implementation of testing :(
|
// Can't mock ImageService partially because of bun incomplete implementation of testing :(
|
||||||
// It goes to the network directly to test
|
// It goes to the network directly to test
|
||||||
const res = await request(app).get("/image");
|
const res = await request(app).get("/image");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 500 if any error happens", async () => {
|
it("should return 500 if any error happens", async () => {
|
||||||
mock.module("src/services/ImageService", () => {
|
mock.module("src/services/ImageService", () => {
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
get: () => {
|
get: () => {
|
||||||
throw new Error("Controlled error");
|
throw new Error("Controlled error");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
});
|
|
||||||
const res = await request(app).get("/image");
|
|
||||||
expect(res.status).toBe(500);
|
|
||||||
});
|
});
|
||||||
})
|
const res = await request(app).get("/image");
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -11,50 +11,58 @@ const gelbooruApiServiceOriginal = GelbooruApiService;
|
||||||
const botApiServiceOriginal = BotApiService;
|
const botApiServiceOriginal = BotApiService;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
mock.module("src/services/ImageService", () => ({
|
mock.module("src/services/ImageService", () => ({
|
||||||
default: imageServiceOriginal,
|
default: imageServiceOriginal,
|
||||||
}));
|
}));
|
||||||
mock.module("src/services/GelbooruApiService", () => ({
|
mock.module("src/services/GelbooruApiService", () => ({
|
||||||
default: gelbooruApiServiceOriginal,
|
default: gelbooruApiServiceOriginal,
|
||||||
}));
|
}));
|
||||||
mock.module("src/services/BotApiService", () => ({
|
mock.module("src/services/BotApiService", () => ({
|
||||||
default: botApiServiceOriginal
|
default: botApiServiceOriginal,
|
||||||
}));
|
}));
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("endpoint gets a non repeated image", () => {
|
describe("endpoint gets a non repeated image", () => {
|
||||||
it("should return an image that is not in the response of the /images endpoint of the bot API", async () => {
|
it("should return an image that is not in the response of the /images endpoint of the bot API", async () => {
|
||||||
const REPEATED_URL = "https://fastly.picsum.photos/id/1/10/20.jpg?hmac=gY6PvUXFacKfYpBpTTVcNLxumpyMmoCamM-J5DOPwNc";
|
const REPEATED_URL =
|
||||||
const UNIQUE_URL = "https://fastly.picsum.photos/id/2/10/20.jpg?hmac=zy6lz21CuRIstr9ETx9h5AuoH50s_L2uIEct3dROpY8";
|
"https://fastly.picsum.photos/id/1/10/20.jpg?hmac=gY6PvUXFacKfYpBpTTVcNLxumpyMmoCamM-J5DOPwNc";
|
||||||
|
const UNIQUE_URL =
|
||||||
|
"https://fastly.picsum.photos/id/2/10/20.jpg?hmac=zy6lz21CuRIstr9ETx9h5AuoH50s_L2uIEct3dROpY8";
|
||||||
|
|
||||||
mock.module("src/services/GelbooruApiService", () => {
|
mock.module("src/services/GelbooruApiService", () => {
|
||||||
let alreadyCalled = false;
|
let alreadyCalled = false;
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
get: (): GelbooruApiResponse => {
|
get: (): GelbooruApiResponse => {
|
||||||
if (alreadyCalled) {
|
if (alreadyCalled) {
|
||||||
return { posts: [{ url: UNIQUE_URL, tags: [] }] };
|
return { posts: [{ url: UNIQUE_URL, tags: [] }] };
|
||||||
} else {
|
} else {
|
||||||
alreadyCalled = true;
|
alreadyCalled = true;
|
||||||
return { posts: [{ url: REPEATED_URL, tags: [] }] };
|
return { posts: [{ url: REPEATED_URL, tags: [] }] };
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
},
|
||||||
mock.module("src/services/BotApiService", () => ({
|
};
|
||||||
default: {
|
|
||||||
getAll: (): BotApiResponse => ({
|
|
||||||
images: [
|
|
||||||
{ _id: "0", url: REPEATED_URL, status: "consumed", tags: ["pokemon", "computer"], "__v": 0 }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const image: Image = await ImageService.get();
|
|
||||||
expect(image.url).not.toBe(REPEATED_URL);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock.module("src/services/BotApiService", () => ({
|
||||||
|
default: {
|
||||||
|
getAll: (): BotApiResponse => ({
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
_id: "0",
|
||||||
|
url: REPEATED_URL,
|
||||||
|
status: "consumed",
|
||||||
|
tags: ["pokemon", "computer"],
|
||||||
|
__v: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const image: Image = await ImageService.get();
|
||||||
|
expect(image.url).not.toBe(REPEATED_URL);
|
||||||
|
});
|
||||||
});
|
});
|
|
@ -18,6 +18,6 @@
|
||||||
"types": [
|
"types": [
|
||||||
"bun-types" // add Bun global
|
"bun-types" // add Bun global
|
||||||
],
|
],
|
||||||
"baseUrl": "./",
|
"baseUrl": "./"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue