Init
This commit is contained in:
commit
b5295dcda8
|
@ -0,0 +1,2 @@
|
||||||
|
NODE_ENV=development
|
||||||
|
LOG_LEVEL=debug
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
|
@ -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: '' })
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { bot } from "./src/bot";
|
||||||
|
import { logger } from "./src/logger";
|
||||||
|
|
||||||
|
bot.updates.start().catch(logger.error);
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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();
|
||||||
|
});
|
|
@ -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));
|
||||||
|
}
|
|
@ -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);
|
||||||
|
};
|
|
@ -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 };
|
|
@ -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);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue