This commit is contained in:
Timofey Gelazoniya 2023-07-16 05:21:26 +03:00
commit b5295dcda8
Signed by: zeldon
GPG Key ID: 047886915281DD2A
12 changed files with 1424 additions and 0 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
NODE_ENV=development
LOG_LEVEL=debug

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
.env

14
env.ts Normal file
View File

@ -0,0 +1,14 @@
import 'dotenv/config';
import { cleanEnv, num, str } from 'envalid';
export const env = cleanEnv(process.env, {
NODE_ENV: str({ choices: ["development", "production"] }),
LOG_LEVEL: str({
choices: ["trace", "debug", "info", "warn", "error", "fatal", "silent"],
}),
LIQUID_RESCALE_API_URL: str({ default: '' }),
BULL_CONCURRENCY: num({ default: 4 }),
REDIS_HOST: str({ default: '' }),
REDIS_PORT: num({ default: 0 }),
VK_BOT_TOKEN: str({ default: '' })
});

4
index.ts Normal file
View File

@ -0,0 +1,4 @@
import { bot } from "./src/bot";
import { logger } from "./src/logger";
bot.updates.start().catch(logger.error);

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"main": "dist/index.js",
"scripts": {
"dev": "vite-node -w index.ts",
"build": "tsc",
"clean": "rimraf dist"
},
"devDependencies": {
"@types/bull": "^4.10.0",
"@types/node": "^20.4.1",
"rimraf": "^5.0.1",
"typescript": "^5.1.6",
"vite-node": "^0.33.0"
},
"dependencies": {
"bull": "^4.10.4",
"dotenv": "^16.3.1",
"envalid": "^7.3.1",
"pino": "^8.14.1",
"pino-pretty": "^10.0.1",
"vk-io": "^4.8.3"
}
}

1212
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

56
src/bot.ts Normal file
View File

@ -0,0 +1,56 @@
import { PhotoAttachment, VK } from "vk-io";
import { env } from "../env";
import { MEDIUM_SIZES, getAttachmentBySizes } from "./utils";
import { makeLiquidRescale } from "./queues/liquid.queue";
export const bot = new VK({ token: env.VK_BOT_TOKEN });
bot.updates.on('message_new', async (context, next) => {
if (context.isChat) return; // ignore chats for now
if (!context.hasAllAttachments('photo')) return;
const processingMessage = await context.reply('Обрабатываю...');
const results = [];
for (let attachment of context.attachments) {
if (attachment.type === 'photo') {
const [size] = getAttachmentBySizes(attachment as PhotoAttachment, MEDIUM_SIZES);
const imageUrl = size?.url;
if (imageUrl) {
const liquidRescaleJob = await makeLiquidRescale({ imageUrl: imageUrl });
let result;
try {
result = await liquidRescaleJob.finished();
} catch (err) {
return context.reply('Упс... что-то пошло не так...');
}
results.push(Buffer.from(result.data));
}
}
}
const attachments: PhotoAttachment[] = [];
for (let image of results) {
const attachment = await bot.upload.messagePhoto({
peer_id: context.senderId,
source: {
values: {
value: image,
contentType: 'image/jpeg'
}
}
});
attachments.push(attachment);
}
await processingMessage.editMessage({ attachment: attachments });
return next();
});

19
src/logger.ts Normal file
View File

@ -0,0 +1,19 @@
import pino, { LoggerOptions } from "pino";
import PinoPretty, { PrettyOptions } from "pino-pretty";
import { env } from "../env";
const options: LoggerOptions = {
level: env.LOG_LEVEL
};
const prettyOptions: PrettyOptions = {
ignore: 'pid,hostname',
colorize: env.isDev ? true : false,
translateTime: 'SYS:dd.mm.yyyy, HH:MM:ss'
};
export let logger = pino(options);
if (env.isDev) {
logger = pino(options, PinoPretty(prettyOptions));
}

View File

@ -0,0 +1,27 @@
import { Job } from "bull";
import { logger } from "../logger";
import { env } from "../../env";
import { fetchImage } from "../utils";
export const liquidRescaleProcess = async (job: Job) => {
logger.info({ id: job.id, data: job.data }, 'Processing liquid rescale job');
const { imageUrl } = job.data;
const image = await fetchImage(imageUrl);
const formData = new FormData();
formData.append('file', new Blob([image]));
let result;
try {
result = await fetchImage(env.LIQUID_RESCALE_API_URL, { method: 'POST', body: formData });
} catch (err) {
logger.error(err);
return Promise.reject(err);
}
// return Promise.resolve({ data: result.toString('base64') });
return Promise.resolve(result);
};

View File

@ -0,0 +1,18 @@
import Bull from "bull";
import { env } from "../../env";
import { liquidRescaleProcess } from "../processes/liquid.process";
export const liquidRescaleQueue = new Bull('liquid-rescale', {
redis: {
host: env.REDIS_HOST,
port: env.REDIS_PORT
}
});
liquidRescaleQueue.process(env.BULL_CONCURRENCY, liquidRescaleProcess);
const makeLiquidRescale = async (data: { imageUrl: string; }) => {
return liquidRescaleQueue.add({ ...data }, { attempts: 3 });
};
export { makeLiquidRescale };

35
src/utils.ts Normal file
View File

@ -0,0 +1,35 @@
import { PhotoAttachment } from "vk-io";
export const SMALL_SIZES = ['m', 's'];
export const MEDIUM_SIZES = ['y', 'r', 'q', 'p', ...SMALL_SIZES];
export const LARGE_SIZES = ['w', 'z', ...MEDIUM_SIZES];
export async function fetchImage(url: string, params?: RequestInit) {
const image = await fetch(url, params)
.then(r => r.arrayBuffer());
const imageBuffer = Buffer.from(image);
return imageBuffer;
}
/**
* Example:
* ```js
* const SMALL_SIZES = ['m', 's'];
* const MEDIUM_SIZES = ['y', 'r', 'q', 'p', ...SMALL_SIZES];
* const LARGE_SIZES = ['w', 'z', ...MEDIUM_SIZES];
*
* const [size] = getAttachmentBySizes(attachment, LARGE_SIZES);
* console.log(size)
* ```
*/
export function getAttachmentBySizes(photoAttachment: PhotoAttachment, sizeTypes: string[] = []) {
const { sizes } = photoAttachment;
if (!sizes) return [];
return sizeTypes
.map((sizeType) => (
sizes.find((size) => size.type === sizeType)
))
.filter(Boolean);
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"outDir": "dist"
}
}