1
0
Fork 0
mirror of https://github.com/muerwre/vk-tg-bot.git synced 2025-04-24 22:46:41 +07:00

Merge branch 'task/#4' into task/#5

This commit is contained in:
Fedor Katurov 2021-05-06 17:20:41 +07:00
commit e2457eb8c8
18 changed files with 140 additions and 89 deletions

View file

@ -23,6 +23,7 @@ templates:
# testResponse: 'testResponseCode' # testResponse: 'testResponseCode'
# secretKey: 'groupSecretKey' # secretKey: 'groupSecretKey'
# apiKey: 'callbackApiKey' # apiKey: 'callbackApiKey'
# post_types: ['post','copy','reply','postpone','suggest']
# channels: # channels:
# - id: '@pogonia_test_chan' # - id: '@pogonia_test_chan'
# events: # events:

View file

@ -10,7 +10,7 @@ export interface Config extends Record<string, any> {
http: HttpConfig; http: HttpConfig;
telegram: TelegramConfig; telegram: TelegramConfig;
vk: VkConfig; vk: VkConfig;
logger?: LoggerConfig; logger: LoggerConfig;
templates?: TemplateConfig; templates: TemplateConfig;
postgres?: PostgresConfig; postgres: PostgresConfig;
} }

View file

@ -9,13 +9,13 @@ export interface Storage {
tgMessageId: number, tgMessageId: number,
groupId: number, groupId: number,
channel: string channel: string
): Promise<Event>; ): Promise<Event | undefined>;
getEventById( getEventById(
type: VkEvent, type: VkEvent,
eventId: number, eventId: number,
groupId: number, groupId: number,
channel: string channel: string
): Promise<Event>; ): Promise<Event | undefined>;
createEvent( createEvent(
type: VkEvent, type: VkEvent,
eventId: number, eventId: number,
@ -23,7 +23,7 @@ export interface Storage {
channel: string, channel: string,
tgMessageId: number, tgMessageId: number,
text: Record<any, any> text: Record<any, any>
): Promise<Event>; ): Promise<Event | undefined>;
createOrUpdateLike( createOrUpdateLike(
messageId: number, messageId: number,
channel: string, channel: string,
@ -31,7 +31,11 @@ export interface Storage {
text: string text: string
): Promise<Like>; ): Promise<Like>;
getLikesFor(channel: string, messageId: number): Promise<Like[]>; getLikesFor(channel: string, messageId: number): Promise<Like[]>;
getLikeBy(channel: string, messageId: number, author: number): Promise<Like>; getLikeBy(
createPost(eventId: number, text: string): Promise<Post>; channel: string,
findPostByEvent(eventId: number): Promise<Post>; messageId: number,
author: number
): Promise<Like | undefined>;
createPost(eventId: number, text: string): Promise<Post | undefined>;
findPostByEvent(eventId: number): Promise<Post | undefined>;
} }

View file

@ -10,21 +10,21 @@ import { VkEvent } from "../../../vk/types";
@Entity() @Entity()
export class Event { export class Event {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@Column() @Column()
type: VkEvent; type!: VkEvent;
@Column() @Column()
vkEventId: number; vkEventId!: number;
@Column() @Column()
vkGroupId: number; vkGroupId!: number;
@Column() @Column()
channel: string; channel!: string;
@Column() @Column()
tgMessageId: number; tgMessageId!: number;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt!: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt!: Date;
@Column("simple-json", { default: {}, nullable: false }) @Column("simple-json", { default: {}, nullable: false })
text: Record<any, any>; text!: Record<any, any>;
} }

View file

@ -9,17 +9,17 @@ import {
@Entity() @Entity()
export class Like { export class Like {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@Column() @Column()
messageId: number; messageId!: number;
@Column() @Column()
channel: string; channel!: string;
@Column({ type: "text" }) @Column({ type: "text" })
text: string; text!: string;
@Column() @Column()
author: number; author!: number;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt!: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt!: Date;
} }

View file

@ -9,13 +9,13 @@ import {
@Entity() @Entity()
export class Post { export class Post {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@Column() @Column()
eventId: number; eventId!: number;
@Column({ type: "text" }) @Column({ type: "text" })
text: string; text!: string;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt!: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt!: Date;
} }

View file

@ -11,10 +11,10 @@ import { Post } from "./entities/Post";
const entities = [path.join(__dirname, "./entities/*")]; const entities = [path.join(__dirname, "./entities/*")];
export class PostgresDB implements Storage { export class PostgresDB implements Storage {
private connection: Connection; private connection!: Connection;
private events: Repository<Event>; private events!: Repository<Event>;
private likes: Repository<Like>; private likes!: Repository<Like>;
private posts: Repository<Post>; private posts!: Repository<Post>;
constructor(private config: PostgresConfig) {} constructor(private config: PostgresConfig) {}

View file

@ -6,7 +6,7 @@ const config = prepareConfig();
const logger = createLogger({ const logger = createLogger({
transports: new transports.Console({ transports: new transports.Console({
format: format.simple(), format: format.simple(),
level: config.logger.level || "info", level: config.logger?.level || "info",
}), }),
}); });

View file

@ -40,7 +40,7 @@ export class TelegramService {
if (isWebhookEnabled) { if (isWebhookEnabled) {
await this.bot.telegram await this.bot.telegram
.deleteWebhook() .deleteWebhook()
.then(() => this.bot.telegram.setWebhook(this.webhook.url)) .then(() => this.bot.telegram.setWebhook(this.webhook.url!))
.then(async () => { .then(async () => {
const info = await this.bot.telegram.getWebhookInfo(); const info = await this.bot.telegram.getWebhookInfo();
if (!info.url) { if (!info.url) {
@ -71,7 +71,7 @@ export class TelegramService {
* Checks webhook availability * Checks webhook availability
*/ */
private getWebhookAvailable = async (): Promise<boolean> => { private getWebhookAvailable = async (): Promise<boolean> => {
const isWebhookEnabled = this.webhook.enabled && this.webhook.url; const isWebhookEnabled = !!this.webhook.enabled && !!this.webhook.url;
// TODO: test this.webhook.url with axios instead of 'true' // TODO: test this.webhook.url with axios instead of 'true'
return isWebhookEnabled && true; return isWebhookEnabled && true;
}; };

View file

@ -1,6 +1,6 @@
import extract from "remark-extract-frontmatter"; import extract from "remark-extract-frontmatter";
import frontmatter from "remark-frontmatter"; import frontmatter from "remark-frontmatter";
import compiler from "retext-stringify"; import stringify from "retext-stringify";
import parser from "remark-parse"; import parser from "remark-parse";
import unified from "unified"; import unified from "unified";
import { parse } from "yaml"; import { parse } from "yaml";
@ -26,7 +26,7 @@ export class Template<
} }
const processor = unified() const processor = unified()
.use(compiler) .use(stringify)
.use(frontmatter) .use(frontmatter)
.use(extract, { yaml: parse }) .use(extract, { yaml: parse })
.use(removeFrontmatter) .use(removeFrontmatter)
@ -43,7 +43,7 @@ export class Template<
} }
/** /**
* Themes the tempalte with values * Themes the template with values
*/ */
public theme = (values: V) => { public theme = (values: V) => {
return hb.compile(this.template)(values); return hb.compile(this.template)(values);
@ -54,6 +54,7 @@ export class Template<
*/ */
public static registerHelpers() { public static registerHelpers() {
hb.registerHelper("ifEq", function (arg1, arg2, options) { hb.registerHelper("ifEq", function (arg1, arg2, options) {
// @ts-ignore
return arg1 == arg2 ? options.fn(this) : options.inverse(this); return arg1 == arg2 ? options.fn(this) : options.inverse(this);
}); });
} }

View file

@ -37,7 +37,7 @@ export class MessageNewHandler extends VkEventHandler<Fields, Values> {
const parsed = this.template.theme({ const parsed = this.template.theme({
user, user,
group: this.group, group: this.group,
text: context.text, text: context?.text || "",
}); });
const extras: ExtraReplyMessage = { const extras: ExtraReplyMessage = {
@ -46,7 +46,7 @@ export class MessageNewHandler extends VkEventHandler<Fields, Values> {
this.appendButtons(extras, user.id); this.appendButtons(extras, user.id);
await this.telegram.sendMessageToChan(this.channel, parsed, extras); await this.telegram.sendMessageToChan(this.channel.id, parsed, extras);
await next(); await next();
}; };

View file

@ -21,7 +21,7 @@ type UrlPrefix = string;
type ExtraGenerator = ( type ExtraGenerator = (
text: string, text: string,
eventId?: number eventId?: number
) => Promise<InlineKeyboardButton[]>; ) => Promise<InlineKeyboardButton[] | undefined>;
interface Fields { interface Fields {
image?: boolean; image?: boolean;
@ -35,6 +35,7 @@ interface Values {
user?: UsersUserFull; user?: UsersUserFull;
group: ConfigGroup; group: ConfigGroup;
text: string; text: string;
type?: string;
} }
type LikeCtx = Composer.Context<CallbackQueryUpdate> & { match: string[] }; type LikeCtx = Composer.Context<CallbackQueryUpdate> & { match: string[] };
@ -52,12 +53,9 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
public execute = async (context: WallPostContext, next: NextMiddleware) => { public execute = async (context: WallPostContext, next: NextMiddleware) => {
const id = context?.wall?.id; const id = context?.wall?.id;
const postType = context?.wall?.postType;
if ( if (context.isRepost || !this.isValidPostType(postType) || !id) {
context.isRepost ||
!PostNewHandler.isValidPostType(context?.wall?.postType) ||
!id
) {
await next(); await next();
return; return;
} }
@ -75,9 +73,9 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
? await this.getUserByID(String(context.wall.signerId)) ? await this.getUserByID(String(context.wall.signerId))
: undefined; : undefined;
const text = context.wall.text.trim(); const text = context.wall?.text?.trim() || "";
const parsed = this.themeText(text, user); const parsed = this.themeText(text, postType, user);
const extras: ExtraReplyMessage = { const extras: ExtraReplyMessage = {
disable_web_page_preview: true, disable_web_page_preview: true,
@ -95,13 +93,17 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
if (hasThumb) { if (hasThumb) {
const thumb = await images.find((img) => img.mediumSizeUrl); const thumb = await images.find((img) => img.mediumSizeUrl);
msg = await this.telegram.sendPhotoToChan( msg = await this.telegram.sendPhotoToChan(
this.channel, this.channel.id,
this.trimTextForPhoto(text, user), this.trimTextForPhoto(text, postType, user),
thumb.mediumSizeUrl, thumb?.mediumSizeUrl!,
extras extras
); );
} else { } else {
msg = await this.telegram.sendMessageToChan(this.channel, parsed, extras); msg = await this.telegram.sendMessageToChan(
this.channel.id,
parsed,
extras
);
} }
const event = await this.createEvent( const event = await this.createEvent(
@ -109,7 +111,8 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
msg.message_id, msg.message_id,
context.wall.toJSON() context.wall.toJSON()
); );
await this.db.createPost(event.id, context.wall.text);
await this.db.createPost(event!.id, context?.wall?.text || "");
await next(); await next();
}; };
@ -117,8 +120,16 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
/** /**
* Checks if event of type we can handle * Checks if event of type we can handle
*/ */
public static isValidPostType(type: string): boolean { private isValidPostType(type?: string): boolean {
return type === "post"; if (!type) {
return false;
}
if (!this.channel.post_types) {
return type === "post";
}
return this.channel.post_types.includes(type);
} }
/** /**
@ -127,7 +138,7 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
private createKeyboard = async ( private createKeyboard = async (
text: string, text: string,
eventId?: number eventId?: number
): Promise<InlineKeyboardMarkup> => { ): Promise<InlineKeyboardMarkup | undefined> => {
const { buttons } = this.template.fields; const { buttons } = this.template.fields;
if (!buttons?.length) { if (!buttons?.length) {
@ -137,7 +148,10 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
const rows = await Promise.all( const rows = await Promise.all(
buttons.map((button) => this.extrasGenerators[button](text, eventId)) buttons.map((button) => this.extrasGenerators[button](text, eventId))
); );
const inline_keyboard = rows.filter((el) => el && el.length);
const inline_keyboard = rows.filter(
(el) => el && el.length
) as InlineKeyboardButton[][];
if (!inline_keyboard.length) { if (!inline_keyboard.length) {
return; return;
@ -153,13 +167,13 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
const links = this.template.fields.links; const links = this.template.fields.links;
if (!links) { if (!links) {
return []; return;
} }
const urls = extractURLs(text); const urls = extractURLs(text);
if (!urls) { if (!urls) {
return []; return;
} }
return urls return urls
@ -170,7 +184,7 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
return label ? { text: links[label], url: url.toString() } : undefined; return label ? { text: links[label], url: url.toString() } : undefined;
}) })
.filter((el) => el); .filter((el) => el) as InlineKeyboardButton[];
}; };
/** /**
@ -179,7 +193,15 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
private generateLikes: ExtraGenerator = async (text, eventId) => { private generateLikes: ExtraGenerator = async (text, eventId) => {
if (eventId) { if (eventId) {
const event = await this.getEventById(eventId); const event = await this.getEventById(eventId);
const likes = await this.db.getLikesFor(this.channel, event.tgMessageId); if (!event) {
throw new Error(`Can't find event`);
}
const likes = await this.db.getLikesFor(
this.channel.id,
event.tgMessageId
);
const withCount = likes.reduce( const withCount = likes.reduce(
(acc, like) => ({ (acc, like) => ({
...acc, ...acc,
@ -209,7 +231,7 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
}; };
/** /**
* Adds needed listeners * Adds needed listeners for telegram
*/ */
protected onInit = () => { protected onInit = () => {
if (this.template.fields.likes) { if (this.template.fields.likes) {
@ -227,7 +249,7 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
* Reacts to like button press * Reacts to like button press
*/ */
private onLikeAction = async (ctx: LikeCtx, next) => { private onLikeAction = async (ctx: LikeCtx, next) => {
const id = ctx.update.callback_query.message.message_id; const id = ctx.update.callback_query?.message?.message_id;
const author = ctx.update.callback_query.from.id; const author = ctx.update.callback_query.from.id;
const [, channel, emo] = ctx.match; const [, channel, emo] = ctx.match;
const event = await this.getEventByTgMessageId(id); const event = await this.getEventByTgMessageId(id);
@ -237,7 +259,7 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
!emo || !emo ||
!id || !id ||
!event || !event ||
channel != this.channel || channel != this.channel.id ||
!this.likes.includes(emo) !this.likes.includes(emo)
) { ) {
await next(); await next();
@ -261,7 +283,7 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
const markup = await this.createKeyboard(post.text, event.id); const markup = await this.createKeyboard(post.text, event.id);
await ctx.telegram.editMessageReplyMarkup( await ctx.telegram.editMessageReplyMarkup(
ctx.chat.id, ctx.chat?.id,
id, id,
ctx.inlineMessageId, ctx.inlineMessageId,
markup markup
@ -274,6 +296,9 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
next(); next();
}; };
/**
* Creates or updates like for {author} on {messageId} with {emo}
*/
private createOrUpdateLike = async ( private createOrUpdateLike = async (
author: number, author: number,
messageId: number, messageId: number,
@ -281,23 +306,31 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
) => { ) => {
return await this.db.createOrUpdateLike( return await this.db.createOrUpdateLike(
messageId, messageId,
this.channel, this.channel.id,
author, author,
emo emo
); );
}; };
/**
* Gets like by {author} on {messageId}
*/
private getLike = async (author: number, messageId: number) => { private getLike = async (author: number, messageId: number) => {
return await this.db.getLikeBy(this.channel, messageId, author); return await this.db.getLikeBy(this.channel.id, messageId, author);
}; };
/** /**
* Applies template theming to photos * Applies template theming to photos
*/ */
private themeText = (text: string, user?: UsersUserFull): string => { private themeText = (
text: string,
type?: string,
user?: UsersUserFull
): string => {
return this.template.theme({ return this.template.theme({
user, user,
group: this.group, group: this.group,
type,
text, text,
}); });
}; };
@ -305,15 +338,24 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
/** /**
* Calculates, how much should we cut off the text to match photo caption limitations * Calculates, how much should we cut off the text to match photo caption limitations
*/ */
private trimTextForPhoto = (text: string, user: UsersUserFull): string => { private trimTextForPhoto = (
// Full markup text: string,
const full = this.themeText(text, user); type?: string,
// Rest info except text user?: UsersUserFull
const others = this.themeText("", user); ): string => {
const withText = this.themeText(text, type, user);
// How much rest markup takes if (withText.length < PHOTO_CAPTION_LIMIT) {
const diff = full.length - others.length; return withText;
}
return full.slice(0, PHOTO_CAPTION_LIMIT - diff); const withoutText = this.themeText("", type, user);
const suffix = "...";
const trimmed = text.slice(
0,
PHOTO_CAPTION_LIMIT - withoutText.length - suffix.length
);
return this.themeText(`${trimmed}${suffix}`, type, user);
}; };
} }

View file

@ -1,5 +1,5 @@
import { NextMiddleware } from "middleware-io"; import { NextMiddleware } from "middleware-io";
import { ConfigGroup, GroupInstance, VkEvent } from "../types"; import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types";
import { VkService } from "../index"; import { VkService } from "../index";
import { TelegramService } from "../../telegram"; import { TelegramService } from "../../telegram";
import { Template } from "../../template"; import { Template } from "../../template";
@ -13,7 +13,7 @@ export class VkEventHandler<
public constructor( public constructor(
protected type: VkEvent, protected type: VkEvent,
protected group: ConfigGroup, protected group: ConfigGroup,
protected channel: string, protected channel: GroupChannel,
protected instance: GroupInstance, protected instance: GroupInstance,
protected vk: VkService, protected vk: VkService,
protected telegram: TelegramService, protected telegram: TelegramService,
@ -60,7 +60,7 @@ export class VkEventHandler<
this.type, this.type,
id, id,
this.group.id, this.group.id,
this.channel this.channel.id
); );
}; };
@ -78,7 +78,7 @@ export class VkEventHandler<
this.type, this.type,
tgMessageId, tgMessageId,
this.group.id, this.group.id,
this.channel this.channel.id
); );
}; };
@ -94,7 +94,7 @@ export class VkEventHandler<
this.type, this.type,
id, id,
this.group.id, this.group.id,
this.channel, this.channel.id,
tgMessageId, tgMessageId,
text text
); );

View file

@ -1,4 +1,4 @@
import { ConfigGroup, GroupInstance, VkEvent } from "../types"; import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types";
import { VkEventHandler } from "./VkEventHandler"; import { VkEventHandler } from "./VkEventHandler";
import { MessageNewHandler } from "./MessageNewHandler"; import { MessageNewHandler } from "./MessageNewHandler";
import { StubHandler } from "./StubHandler"; import { StubHandler } from "./StubHandler";
@ -12,7 +12,7 @@ interface Handler {
new ( new (
type: VkEvent, type: VkEvent,
group: ConfigGroup, group: ConfigGroup,
channel: string, channel: GroupChannel,
instance: GroupInstance, instance: GroupInstance,
vk: VkService, vk: VkService,
telegram: TelegramService, telegram: TelegramService,

View file

@ -29,7 +29,7 @@ export class VkService {
throw new Error("No vk groups to handle. Specify them in config"); throw new Error("No vk groups to handle. Specify them in config");
} }
this.endpoint = config.endpoint; this.endpoint = config.endpoint || "/";
this.groups = config.groups.reduce( this.groups = config.groups.reduce(
(acc, group) => ({ (acc, group) => ({
@ -121,7 +121,7 @@ export class VkService {
const handler = new vkEventToHandler[event]( const handler = new vkEventToHandler[event](
event, event,
group, group,
chan.id, chan,
instance, instance,
this, this,
this.telegram, this.telegram,

View file

@ -1,4 +1,5 @@
import { API, Upload, Updates } from "vk-io"; import { API, Upload, Updates } from "vk-io";
import { WallPostType } from "vk-io/lib/api/schemas/objects";
export interface VkConfig extends Record<string, any> { export interface VkConfig extends Record<string, any> {
groups: ConfigGroup[]; groups: ConfigGroup[];
@ -17,6 +18,7 @@ export interface ConfigGroup {
export interface GroupChannel { export interface GroupChannel {
id: string; id: string;
events: VkEvent[]; events: VkEvent[];
post_types: WallPostType;
} }
export enum VkEvent { export enum VkEvent {

View file

@ -13,5 +13,5 @@ export const extractURLs = (text: string): URL[] => {
return; return;
} }
}) })
.filter((el) => el); .filter((el) => el) as URL[];
}; };

View file

@ -12,7 +12,8 @@
"target": "es2017", "target": "es2017",
"baseUrl": ".", "baseUrl": ".",
"paths": {}, "paths": {},
"lib": ["esnext"] "lib": ["esnext"],
"strict": true
}, },
"include": [ "include": [
"./**/*", "./**/*",