Init
This commit is contained in:
commit
b5295dcda8
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=debug
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
14
env.ts
Normal file
14
env.ts
Normal 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
4
index.ts
Normal 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
23
package.json
Normal 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
generated
Normal file
1212
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
src/bot.ts
Normal file
56
src/bot.ts
Normal 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
19
src/logger.ts
Normal 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));
|
||||
}
|
27
src/processes/liquid.process.ts
Normal file
27
src/processes/liquid.process.ts
Normal 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);
|
||||
};
|
18
src/queues/liquid.queue.ts
Normal file
18
src/queues/liquid.queue.ts
Normal 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
35
src/utils.ts
Normal 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
11
tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user