initial commit
This commit is contained in:
commit
4673348f38
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
esbuild: {
|
||||||
|
platform: 'node',
|
||||||
|
target: 'node16',
|
||||||
|
bundle: process.env.NODE_ENV === 'production' ? true : false
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"watch": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"ext": "ts,js,json",
|
||||||
|
"exec": "etsc && node ./dist/index.js",
|
||||||
|
"legacyWatch": true
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "balabola-bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon",
|
||||||
|
"build": "rm -rf ./dist && tsc --noEmit && NODE_ENV=production etsc",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"envalid": "^7.3.1",
|
||||||
|
"middleware-io": "^2.8.1",
|
||||||
|
"p-queue": "^6.6.2",
|
||||||
|
"pino": "^8.7.0",
|
||||||
|
"pino-pretty": "^9.1.1",
|
||||||
|
"undici": "^5.11.0",
|
||||||
|
"vk-io": "^4.7.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.11.2",
|
||||||
|
"esbuild": "^0.15.12",
|
||||||
|
"esbuild-node-tsc": "^2.0.4",
|
||||||
|
"nodemon": "^2.0.20",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { fetch } from 'undici';
|
||||||
|
|
||||||
|
interface BalabolaResponse {
|
||||||
|
bad_query: number,
|
||||||
|
query: string,
|
||||||
|
text: string,
|
||||||
|
error: number,
|
||||||
|
is_cached: number,
|
||||||
|
empty_zeliboba: number,
|
||||||
|
intro: number,
|
||||||
|
signature: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_URL = 'https://yandex.ru/lab/api/yalm/text3';
|
||||||
|
|
||||||
|
export async function balabola(query: string, intro: number): Promise<string> {
|
||||||
|
const result = await fetch(BASE_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
filter: 1,
|
||||||
|
intro,
|
||||||
|
query
|
||||||
|
})
|
||||||
|
}).then(res => res.json()) as BalabolaResponse;
|
||||||
|
|
||||||
|
return result.text;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { VK } from "vk-io";
|
||||||
|
import { env } from "./env";
|
||||||
|
import handlers from "./handlers";
|
||||||
|
|
||||||
|
export const bot = new VK({
|
||||||
|
token: env.BOT_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.updates.on('message_new', handlers.compose());
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { BranchMiddlewareCondition, Middleware, MiddlewareReturn, NextMiddleware, UnknownObject } from 'middleware-io';
|
||||||
|
import { Composer as BaseComposer, MessageContext } from 'vk-io';
|
||||||
|
|
||||||
|
declare module 'middleware-io' {
|
||||||
|
class Composer<T extends UnknownObject, R = T> {
|
||||||
|
filter<V = UnknownObject>(condition: BranchMiddlewareCondition<T & V>, filterMiddleware: Middleware<T & V>): this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllowArray<T> = T | T[];
|
||||||
|
|
||||||
|
type HearFunctionCondition<T, V> = (value: V, context: T) => boolean;
|
||||||
|
type HearCondition<T, V> = HearFunctionCondition<T, V> | RegExp | string | number | boolean;
|
||||||
|
|
||||||
|
type HearObjectCondition<T extends Record<string, any>> =
|
||||||
|
Record<string, AllowArray<HearCondition<T, any>>>
|
||||||
|
& {
|
||||||
|
[P in keyof T]?: AllowArray<HearCondition<T, T[P]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HearConditions<T extends Record<string, any>> = (
|
||||||
|
AllowArray<HearCondition<T, string | undefined>>
|
||||||
|
| AllowArray<HearObjectCondition<T>>
|
||||||
|
);
|
||||||
|
|
||||||
|
function splitPath(path: string): string[] {
|
||||||
|
return (
|
||||||
|
path
|
||||||
|
.replace(/\[([^[\]]*)\]/g, '.$1.')
|
||||||
|
.split('.')
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectValue(source: Record<string, any>, selectors: string[]): any {
|
||||||
|
let link = source;
|
||||||
|
|
||||||
|
for (const selector of selectors) {
|
||||||
|
if (!link[selector]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
link = link[selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unifyCondition(condition: unknown): Function {
|
||||||
|
if (typeof condition === 'function') {
|
||||||
|
return condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition instanceof RegExp) {
|
||||||
|
return (text: string | undefined): boolean => (
|
||||||
|
condition.test(text!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(condition)) {
|
||||||
|
const arrayConditions = condition.map(unifyCondition);
|
||||||
|
|
||||||
|
return (value: string | undefined): boolean => (
|
||||||
|
Array.isArray(value)
|
||||||
|
? arrayConditions.every((cond): boolean => (
|
||||||
|
value.some((val): boolean => cond(val))
|
||||||
|
))
|
||||||
|
: arrayConditions.some((cond): boolean => (
|
||||||
|
cond(value)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (value: string | undefined): boolean => value === condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Composer<C extends MessageContext, R = C> extends BaseComposer<C, R> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hear<T = {}>(
|
||||||
|
hearConditions: HearConditions<C & T>,
|
||||||
|
handler: Middleware<C & T>
|
||||||
|
): this {
|
||||||
|
const rawConditions = !Array.isArray(hearConditions)
|
||||||
|
? [hearConditions]
|
||||||
|
: hearConditions;
|
||||||
|
|
||||||
|
const hasConditions = rawConditions.every(Boolean);
|
||||||
|
|
||||||
|
if (!hasConditions) {
|
||||||
|
throw new Error('Condition should be not empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof handler !== 'function') {
|
||||||
|
throw new TypeError('Handler must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
let textCondition = false;
|
||||||
|
let functionCondtion = false;
|
||||||
|
const conditions = rawConditions.map((condition): Function => {
|
||||||
|
if (typeof condition === 'object' && !(condition instanceof RegExp)) {
|
||||||
|
functionCondtion = true;
|
||||||
|
|
||||||
|
const entries = Object.entries(condition).map(([path, value]): [string[], Function] => (
|
||||||
|
[splitPath(path), unifyCondition(value)]
|
||||||
|
));
|
||||||
|
|
||||||
|
return (text: string | undefined, context: C): boolean => (
|
||||||
|
entries.every(([selectors, callback]): boolean => {
|
||||||
|
const value = getObjectValue(context, selectors);
|
||||||
|
|
||||||
|
return callback(value, context);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof condition === 'function') {
|
||||||
|
functionCondtion = true;
|
||||||
|
|
||||||
|
return condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
textCondition = true;
|
||||||
|
|
||||||
|
if (condition instanceof RegExp) {
|
||||||
|
return (text: string | undefined, context: C): boolean => {
|
||||||
|
const passed = condition.test(text!);
|
||||||
|
|
||||||
|
if (passed) {
|
||||||
|
context.$match = text!.match(condition)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return passed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringCondition = String(condition);
|
||||||
|
|
||||||
|
return (text: string | undefined): boolean => text === stringCondition;
|
||||||
|
});
|
||||||
|
|
||||||
|
const needText = textCondition && functionCondtion === false;
|
||||||
|
|
||||||
|
this.use((context: C & T, next: NextMiddleware): MiddlewareReturn => {
|
||||||
|
const { text } = context;
|
||||||
|
|
||||||
|
if (needText && text === undefined) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSome = conditions.some((condition): boolean => (
|
||||||
|
condition(text, context)
|
||||||
|
));
|
||||||
|
|
||||||
|
return hasSome
|
||||||
|
? handler(context, next)
|
||||||
|
: next();
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { cleanEnv, 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"],
|
||||||
|
}),
|
||||||
|
BOT_TOKEN: str(),
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { MessageContext } from "vk-io";
|
||||||
|
|
||||||
|
type MaybePromise<T> = T | Promise<T>;
|
||||||
|
type Predicate<C extends MessageContext> = (ctx: C) => MaybePromise<boolean>;
|
||||||
|
|
||||||
|
export const not = <C extends MessageContext>(predicate: Predicate<C>) => (ctx: C) =>
|
||||||
|
Promise.resolve(predicate(ctx)).then((v) => !v);
|
||||||
|
|
||||||
|
export const isChat = <C extends MessageContext>(ctx: C) =>
|
||||||
|
ctx.isChat === true;
|
||||||
|
|
||||||
|
export const isHasText = <C extends MessageContext>(ctx: C) =>
|
||||||
|
ctx.text ? true : false;
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Keyboard, MessageContext } from "vk-io";
|
||||||
|
import { balabola } from "../balabola_api";
|
||||||
|
import { Composer } from "../composer";
|
||||||
|
import { isChat, isHasText, not } from "../filters";
|
||||||
|
import { balabolaQueue } from "../queue";
|
||||||
|
|
||||||
|
export const composer = new Composer<MessageContext>();
|
||||||
|
|
||||||
|
const filter = composer
|
||||||
|
.filter(not(isChat), composer.compose())
|
||||||
|
.filter(!!not(isHasText), composer.compose());
|
||||||
|
|
||||||
|
const selectStyleKeyboard = <C extends MessageContext>(ctx: C) => {
|
||||||
|
return Keyboard.builder()
|
||||||
|
.textButton({ label: '1', payload: { command: 'пр 0 ' + ctx.text! } })
|
||||||
|
.textButton({ label: '2', payload: { command: 'пр 24 ' + ctx.text! } })
|
||||||
|
.textButton({ label: '3', payload: { command: 'пр 25 ' + ctx.text! } })
|
||||||
|
.textButton({ label: '4', payload: { command: 'пр 11 ' + ctx.text! } })
|
||||||
|
.row()
|
||||||
|
.textButton({ label: '5', payload: { command: 'пр 6 ' + ctx.text! } })
|
||||||
|
.textButton({ label: '6', payload: { command: 'пр 8 ' + ctx.text! } })
|
||||||
|
.textButton({ label: '7', payload: { command: 'пр 9 ' + ctx.text! } })
|
||||||
|
.row()
|
||||||
|
.textButton({ label: 'чзх', payload: { command: 'help' } })
|
||||||
|
.inline();
|
||||||
|
};
|
||||||
|
|
||||||
|
const balabolaMessage = `Выбери стилизацию ответа:
|
||||||
|
1: без стилизации
|
||||||
|
2: инструкция
|
||||||
|
3: рецепт
|
||||||
|
4: мудрость
|
||||||
|
5: история
|
||||||
|
6: википедия
|
||||||
|
7: синопсис
|
||||||
|
более подробно команда help`;
|
||||||
|
|
||||||
|
filter.hear(/^(?:продолжи)\s(.*)?$/i, async ctx => {
|
||||||
|
await ctx.send(balabolaMessage, {
|
||||||
|
keyboard: selectStyleKeyboard(ctx)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.hear(/^(?:пр)\s(0|24|25|11|6|8|9)\s(.*)?$/i, async ctx => {
|
||||||
|
const intro = ctx.$match[1];
|
||||||
|
const query = ctx.$match[2];
|
||||||
|
|
||||||
|
await ctx.send('генерирую!!');
|
||||||
|
|
||||||
|
const result = await balabolaQueue.add(() => balabola(query, +intro));
|
||||||
|
await ctx.send(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.use(async ctx => {
|
||||||
|
await ctx.send('генерирую!!');
|
||||||
|
const result = await balabolaQueue.add(() => balabola(ctx.text!, 6));
|
||||||
|
await ctx.send(result);
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { APIError, MessageContext } from "vk-io";
|
||||||
|
import { Composer } from "../composer";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
export const composer = new Composer<MessageContext>();
|
||||||
|
|
||||||
|
composer.use(async (ctx, next) => {
|
||||||
|
try {
|
||||||
|
await next();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
composer.use(async (ctx, next) => {
|
||||||
|
try {
|
||||||
|
await next();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof APIError && err.code === 917) {
|
||||||
|
logger.error('no access to the chat');
|
||||||
|
await ctx.send('хачю доступ к чату :=(');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Composer } from "../composer";
|
||||||
|
import { MessageContext } from "vk-io";
|
||||||
|
|
||||||
|
export const composer = new Composer<MessageContext>();
|
||||||
|
|
||||||
|
const helpMessage = `эта штука работает через балабола (https://yandex.ru/lab/yalm)
|
||||||
|
настройки стилизации:
|
||||||
|
1: без стиля
|
||||||
|
- ну тут все и так понятно
|
||||||
|
2: инструкции по применению
|
||||||
|
- Перечислите несколько предметов, а Балабоба придумает, как их использовать
|
||||||
|
3: рецепты
|
||||||
|
- Перечислите съедобные ингредиенты, а Балабоба придумает рецепт с ними
|
||||||
|
4: мудрости
|
||||||
|
- Напишите что-нибудь и получите народную мудрость
|
||||||
|
5: истории
|
||||||
|
- Начните писать историю, а Балабобы продолжит — иногда страшно, но чаще смешно
|
||||||
|
6: википедия
|
||||||
|
- Напишите какое-нибудь слово, а Балабоба даст этому определение
|
||||||
|
7: синопсис
|
||||||
|
- Напишите название фильма (существующего или нет), а Балабоба расскажет вам, о чем он`;
|
||||||
|
|
||||||
|
composer.hear(/^(?:start|помощь|старт|начать|help)$/i, async ctx => {
|
||||||
|
await ctx.send(helpMessage, {
|
||||||
|
dont_parse_links: true
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Composer } from "../composer";
|
||||||
|
import { MessageContext } from "vk-io";
|
||||||
|
import { composer as updatesHandler } from "./updates";
|
||||||
|
import { composer as helpHandler } from "./help";
|
||||||
|
import { composer as balabolaHandler } from "./balabola";
|
||||||
|
import { composer as errorHandler } from "./error";
|
||||||
|
|
||||||
|
const handlers = new Composer<MessageContext>();
|
||||||
|
export default handlers;
|
||||||
|
|
||||||
|
handlers.use(errorHandler.compose());
|
||||||
|
handlers.use(updatesHandler.compose());
|
||||||
|
handlers.use(helpHandler.compose());
|
||||||
|
handlers.use(balabolaHandler.compose());
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { MessageContext } from "vk-io";
|
||||||
|
import { Composer } from "../composer";
|
||||||
|
import { isHasText, not } from "../filters";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
export const composer = new Composer<MessageContext>();
|
||||||
|
const filter = composer.filter(!!not(isHasText), composer.compose());
|
||||||
|
|
||||||
|
filter.use(async (ctx, next) => {
|
||||||
|
const { messagePayload } = ctx;
|
||||||
|
|
||||||
|
ctx.state.command = messagePayload && messagePayload.command
|
||||||
|
? messagePayload.command
|
||||||
|
: null;
|
||||||
|
|
||||||
|
logger.debug({
|
||||||
|
payload: messagePayload,
|
||||||
|
state: ctx.state,
|
||||||
|
text: ctx.text
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ctx.state.command) ctx.text = ctx.state.command;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { bot } from "./bot";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
|
bot.updates.start().catch(logger.error).then(() => logger.info('bot started'));
|
|
@ -0,0 +1,21 @@
|
||||||
|
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 rawLogger = pino(options);
|
||||||
|
|
||||||
|
if (env.isDev) {
|
||||||
|
rawLogger = pino(options, PinoPretty(prettyOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = rawLogger.child({ name: 'balabola' });
|
|
@ -0,0 +1,11 @@
|
||||||
|
import PQueue from 'p-queue';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
export const balabolaQueue = new PQueue({
|
||||||
|
concurrency: 1,
|
||||||
|
interval: 2000,
|
||||||
|
intervalCap: 1,
|
||||||
|
carryoverConcurrencyCount: true
|
||||||
|
});
|
||||||
|
|
||||||
|
balabolaQueue.on('add', () => logger.info('new balabola job added to queue'));
|
|
@ -0,0 +1,5 @@
|
||||||
|
function isNumber(value: string | number): boolean {
|
||||||
|
return ((value != null) &&
|
||||||
|
(value !== '') &&
|
||||||
|
!isNaN(Number(value.toString())));
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"removeComments": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types"
|
||||||
|
],
|
||||||
|
"sourceMap": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"lib": [
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"lib/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
}
|
Loading…
Reference in New Issue