diff --git a/compose.yml b/compose.yml index 5e72207..9c48217 100644 --- a/compose.yml +++ b/compose.yml @@ -10,7 +10,7 @@ services: MONGO_INITDB_DATABASE: bot volumes: - mongodb_data:/data/db - + bot-api: image: git.fai.st/fedi-image-bot/bot-api:latest container_name: bot-api @@ -38,8 +38,8 @@ services: environment: PORT: 8081 BOT_API_URI: "http://bot-api:8080" - 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_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 volumes: - ./:/usr/src/app:ro diff --git a/package.json b/package.json index 0708b23..8b9f352 100644 --- a/package.json +++ b/package.json @@ -24,4 +24,4 @@ "dev": "docker compose down -v && docker compose up", "test": "docker compose down -v && docker compose run fe-middleware 'sleep 5 && bun test'" } -} \ No newline at end of file +} diff --git a/src/app.ts b/src/app.ts index 893a90b..8edee2b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,10 +9,10 @@ app.use(express.json()); app.use(compression()); app.get("/", (_, res) => { - const endpoints = listEndpoints(app); - res.json({ endpoints }); + const endpoints = listEndpoints(app); + res.json({ endpoints }); }); app.get("/image", ImageController.get); -export default app; \ No newline at end of file +export default app; diff --git a/src/controllers/ImageController.ts b/src/controllers/ImageController.ts index 18d010d..9a6bcf5 100644 --- a/src/controllers/ImageController.ts +++ b/src/controllers/ImageController.ts @@ -3,15 +3,15 @@ import logger from "src/logger"; import ImageService from "src/services/ImageService"; class ImageController { - async get(_: Request, res: Response) { - try { - const image = await ImageService.get(); - res.json(image); - } catch (error: any) { - logger.error(error) - res.status(500).json({ "error": `Internal server error: ${error}` }); - } + async get(_: Request, res: Response) { + try { + const image = await ImageService.get(); + res.json(image); + } catch (error: any) { + logger.error(error); + res.status(500).json({ error: `Internal server error: ${error}` }); } + } } -export default new ImageController(); \ No newline at end of file +export default new ImageController(); diff --git a/src/index.ts b/src/index.ts index 50cac35..8435127 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,12 +5,12 @@ import logger from "src/logger"; const PORT = env.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", () => { - server.close(() => { - logger.info('Server closed.'); - process.exit(0); - }); -}); \ No newline at end of file + server.close(() => { + logger.info("Server closed."); + process.exit(0); + }); +}); diff --git a/src/services/BotApiService.ts b/src/services/BotApiService.ts index 2de1249..85b0b8b 100644 --- a/src/services/BotApiService.ts +++ b/src/services/BotApiService.ts @@ -3,22 +3,20 @@ import logger from "src/logger"; import { BotApiResponse } from "src/types/BotApiResponse"; class BotApiService { - readonly BOT_API_URI = env.BOT_API_URI; - - async getAll(): Promise { - 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; - } + readonly BOT_API_URI = env.BOT_API_URI; + async getAll(): Promise { + 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(); \ No newline at end of file +export default new BotApiService(); diff --git a/src/services/GelbooruApiService.ts b/src/services/GelbooruApiService.ts index be70475..509f472 100644 --- a/src/services/GelbooruApiService.ts +++ b/src/services/GelbooruApiService.ts @@ -4,23 +4,29 @@ import GelbooruApiResponse from "src/types/GelbooruApiResponse"; import GelbooruServiceResponse from "src/types/GelbooruServiceResponse"; class GelbooruApiService { - async get(): Promise { - const LIMIT = env.GELBOORU_IMAGES_PER_REQUEST || 100; - 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}`; + async get(): Promise { + const LIMIT = env.GELBOORU_IMAGES_PER_REQUEST || 100; + 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 response: GelbooruApiResponse = await fetch(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 GelbooruApiResponse; + const response: GelbooruApiResponse = (await fetch(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 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(); \ No newline at end of file +export default new GelbooruApiService(); diff --git a/src/services/ImageService.ts b/src/services/ImageService.ts index 00ab51d..53005fb 100644 --- a/src/services/ImageService.ts +++ b/src/services/ImageService.ts @@ -5,32 +5,35 @@ import GelbooruApiService from "src/services/GelbooruApiService"; import logger from "src/logger"; class ImageService { - postsQueue: Image[] = []; + postsQueue: Image[] = []; - async get(): Promise { - while (this.postsQueue.length === 0) { - const validPosts = await this.getNewValidImages(); - this.postsQueue = validPosts; - logger.info(`Got ${validPosts.length} images from remote`) - } - return this.postsQueue.pop() as Image; + async get(): Promise { + while (this.postsQueue.length === 0) { + const validPosts = await this.getNewValidImages(); + this.postsQueue = validPosts; + logger.info(`Got ${validPosts.length} images from remote`); } + return this.postsQueue.pop() as Image; + } - private async getNewValidImages(): Promise { - const gelbooruResponse: GelbooruServiceResponse = await GelbooruApiService.get(); - const posts = gelbooruResponse.posts; + private async getNewValidImages(): Promise { + const gelbooruResponse: GelbooruServiceResponse = + await GelbooruApiService.get(); + const posts = gelbooruResponse.posts; - const botResponse = await BotApiService.getAll(); - const imagesUrls = botResponse.images.map(image => image.url); + const botResponse = await BotApiService.getAll(); + const imagesUrls = botResponse.images.map((image) => image.url); - const validPosts = Promise.all(posts - .filter(post => !imagesUrls.some(url => url === post.url)) - .map((post): Image => { - return { url: post.url, tags: post.tags }; - })); - - return validPosts; - } + const validPosts = Promise.all( + posts + .filter((post) => !imagesUrls.some((url) => url === post.url)) + .map((post): Image => { + return { url: post.url, tags: post.tags }; + }) + ); + + return validPosts; + } } -export default new ImageService(); \ No newline at end of file +export default new ImageService(); diff --git a/src/types/BotApiResponse.ts b/src/types/BotApiResponse.ts index 1076bee..0d4f934 100644 --- a/src/types/BotApiResponse.ts +++ b/src/types/BotApiResponse.ts @@ -1,5 +1,5 @@ import BotImage from "src/types/BotImage"; export interface BotApiResponse { - images: BotImage[] -} \ No newline at end of file + images: BotImage[]; +} diff --git a/src/types/BotImage.ts b/src/types/BotImage.ts index ded068e..486d61e 100644 --- a/src/types/BotImage.ts +++ b/src/types/BotImage.ts @@ -1,7 +1,7 @@ export default interface BotImage { - _id: string - url: string - status: string - tags: string[] - __v: number + _id: string; + url: string; + status: string; + tags: string[]; + __v: number; } diff --git a/src/types/GelbooruApiResponse.ts b/src/types/GelbooruApiResponse.ts index b8b93d6..875207b 100644 --- a/src/types/GelbooruApiResponse.ts +++ b/src/types/GelbooruApiResponse.ts @@ -1,42 +1,42 @@ export default interface GelbooruApiResponse { - "@attributes": Attributes - post: Post[] + "@attributes": Attributes; + post: Post[]; } - + export interface Attributes { - limit: number - offset: number - count: number + limit: number; + offset: number; + count: number; } export interface Post { - id: number - created_at: string - score: number - width: number - height: number - md5: string - directory: string - image: string - rating: string - source: string - change: number - owner: string - creator_id: number - parent_id: number - sample: number - preview_height: number - preview_width: number - tags: string - title: string - has_notes: string - has_comments: string - file_url: string - preview_url: string - sample_url: string - sample_height: number - sample_width: number - status: string - post_locked: number - has_children: string + id: number; + created_at: string; + score: number; + width: number; + height: number; + md5: string; + directory: string; + image: string; + rating: string; + source: string; + change: number; + owner: string; + creator_id: number; + parent_id: number; + sample: number; + preview_height: number; + preview_width: number; + tags: string; + title: string; + has_notes: string; + has_comments: string; + file_url: string; + preview_url: string; + sample_url: string; + sample_height: number; + sample_width: number; + status: string; + post_locked: number; + has_children: string; } diff --git a/src/types/GelbooruPost.ts b/src/types/GelbooruPost.ts index 775adbb..feb9ae6 100644 --- a/src/types/GelbooruPost.ts +++ b/src/types/GelbooruPost.ts @@ -1,4 +1,4 @@ export default interface GelbooruPost { - url: string; - tags: string[]; -} \ No newline at end of file + url: string; + tags: string[]; +} diff --git a/src/types/GelbooruServiceResponse.ts b/src/types/GelbooruServiceResponse.ts index 4bfc75f..050962b 100644 --- a/src/types/GelbooruServiceResponse.ts +++ b/src/types/GelbooruServiceResponse.ts @@ -1,5 +1,5 @@ import GelbooruPost from "src/types/GelbooruPost"; export default interface GelbooruServiceResponse { - posts: GelbooruPost[]; -} \ No newline at end of file + posts: GelbooruPost[]; +} diff --git a/src/types/Image.ts b/src/types/Image.ts index 1f2ab10..19b3c2f 100644 --- a/src/types/Image.ts +++ b/src/types/Image.ts @@ -1,4 +1,4 @@ export default interface Image { - url: string; - tags: string[]; -} \ No newline at end of file + url: string; + tags: string[]; +} diff --git a/test/ImageController/ImageController.test.ts b/test/ImageController/ImageController.test.ts index 0b4e99a..ddf1ad5 100644 --- a/test/ImageController/ImageController.test.ts +++ b/test/ImageController/ImageController.test.ts @@ -6,31 +6,31 @@ import request from "supertest"; const imageServiceOriginal = ImageService; afterEach(() => { - mock.restore(); - mock.module("src/services/ImageService", () => ({ - default: imageServiceOriginal, - })); -}) + mock.restore(); + mock.module("src/services/ImageService", () => ({ + default: imageServiceOriginal, + })); +}); describe("endpoint returns the correct status codes", () => { - it("should return 200 if successful", async () => { - // Can't mock ImageService partially because of bun incomplete implementation of testing :( - // It goes to the network directly to test - const res = await request(app).get("/image"); - expect(res.status).toBe(200); - }); + it("should return 200 if successful", async () => { + // Can't mock ImageService partially because of bun incomplete implementation of testing :( + // It goes to the network directly to test + const res = await request(app).get("/image"); + expect(res.status).toBe(200); + }); - it("should return 500 if any error happens", async () => { - mock.module("src/services/ImageService", () => { - return { - default: { - get: () => { - throw new Error("Controlled error"); - } - } - } - }); - const res = await request(app).get("/image"); - expect(res.status).toBe(500); + it("should return 500 if any error happens", async () => { + mock.module("src/services/ImageService", () => { + return { + default: { + get: () => { + throw new Error("Controlled error"); + }, + }, + }; }); -}) \ No newline at end of file + const res = await request(app).get("/image"); + expect(res.status).toBe(500); + }); +}); diff --git a/test/ImageService/ImageService.test.ts b/test/ImageService/ImageService.test.ts index d13f7aa..1f48d98 100644 --- a/test/ImageService/ImageService.test.ts +++ b/test/ImageService/ImageService.test.ts @@ -11,50 +11,58 @@ const gelbooruApiServiceOriginal = GelbooruApiService; const botApiServiceOriginal = BotApiService; afterEach(() => { - mock.restore(); - mock.module("src/services/ImageService", () => ({ - default: imageServiceOriginal, - })); - mock.module("src/services/GelbooruApiService", () => ({ - default: gelbooruApiServiceOriginal, - })); - mock.module("src/services/BotApiService", () => ({ - default: botApiServiceOriginal - })); -}) + mock.restore(); + mock.module("src/services/ImageService", () => ({ + default: imageServiceOriginal, + })); + mock.module("src/services/GelbooruApiService", () => ({ + default: gelbooruApiServiceOriginal, + })); + mock.module("src/services/BotApiService", () => ({ + default: botApiServiceOriginal, + })); +}); 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 () => { - const REPEATED_URL = "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"; + 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 UNIQUE_URL = + "https://fastly.picsum.photos/id/2/10/20.jpg?hmac=zy6lz21CuRIstr9ETx9h5AuoH50s_L2uIEct3dROpY8"; - mock.module("src/services/GelbooruApiService", () => { - let alreadyCalled = false; - return { - default: { - get: (): GelbooruApiResponse => { - if (alreadyCalled) { - return { posts: [{ url: UNIQUE_URL, tags: [] }] }; - } else { - alreadyCalled = true; - return { posts: [{ url: REPEATED_URL, tags: [] }] }; - } - } - } + mock.module("src/services/GelbooruApiService", () => { + let alreadyCalled = false; + return { + default: { + get: (): GelbooruApiResponse => { + if (alreadyCalled) { + return { posts: [{ url: UNIQUE_URL, tags: [] }] }; + } else { + alreadyCalled = true; + 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); + }, + }, + }; }); -}); \ No newline at end of file + + 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); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index eeeb347..440a256 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,6 @@ "types": [ "bun-types" // add Bun global ], - "baseUrl": "./", + "baseUrl": "./" } }