diff --git a/config.example.yml b/config.example.yml index 0c07a68..e7b3d67 100644 --- a/config.example.yml +++ b/config.example.yml @@ -23,6 +23,7 @@ templates: # testResponse: 'testResponseCode' # secretKey: 'groupSecretKey' # apiKey: 'callbackApiKey' +# post_types: ['post','copy','reply','postpone','suggest'] # channels: # - id: '@pogonia_test_chan' # events: diff --git a/src/config/types.ts b/src/config/types.ts index 9e242ac..d64ade2 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -10,7 +10,7 @@ export interface Config extends Record { http: HttpConfig; telegram: TelegramConfig; vk: VkConfig; - logger?: LoggerConfig; - templates?: TemplateConfig; - postgres?: PostgresConfig; + logger: LoggerConfig; + templates: TemplateConfig; + postgres: PostgresConfig; } diff --git a/src/service/db/index.ts b/src/service/db/index.ts index 4bc1a53..625f9f3 100644 --- a/src/service/db/index.ts +++ b/src/service/db/index.ts @@ -9,13 +9,13 @@ export interface Storage { tgMessageId: number, groupId: number, channel: string - ): Promise; + ): Promise; getEventById( type: VkEvent, eventId: number, groupId: number, channel: string - ): Promise; + ): Promise; createEvent( type: VkEvent, eventId: number, @@ -23,7 +23,7 @@ export interface Storage { channel: string, tgMessageId: number, text: Record - ): Promise; + ): Promise; createOrUpdateLike( messageId: number, channel: string, @@ -31,7 +31,11 @@ export interface Storage { text: string ): Promise; getLikesFor(channel: string, messageId: number): Promise; - getLikeBy(channel: string, messageId: number, author: number): Promise; - createPost(eventId: number, text: string): Promise; - findPostByEvent(eventId: number): Promise; + getLikeBy( + channel: string, + messageId: number, + author: number + ): Promise; + createPost(eventId: number, text: string): Promise; + findPostByEvent(eventId: number): Promise; } diff --git a/src/service/db/postgres/entities/Event.ts b/src/service/db/postgres/entities/Event.ts index 9f84ba2..1bab06d 100644 --- a/src/service/db/postgres/entities/Event.ts +++ b/src/service/db/postgres/entities/Event.ts @@ -10,21 +10,21 @@ import { VkEvent } from "../../../vk/types"; @Entity() export class Event { @PrimaryGeneratedColumn() - id: number; + id!: number; @Column() - type: VkEvent; + type!: VkEvent; @Column() - vkEventId: number; + vkEventId!: number; @Column() - vkGroupId: number; + vkGroupId!: number; @Column() - channel: string; + channel!: string; @Column() - tgMessageId: number; + tgMessageId!: number; @CreateDateColumn() - createdAt: Date; + createdAt!: Date; @UpdateDateColumn() - updatedAt: Date; + updatedAt!: Date; @Column("simple-json", { default: {}, nullable: false }) - text: Record; + text!: Record; } diff --git a/src/service/db/postgres/entities/Like.ts b/src/service/db/postgres/entities/Like.ts index ff1c2d3..c723d07 100644 --- a/src/service/db/postgres/entities/Like.ts +++ b/src/service/db/postgres/entities/Like.ts @@ -9,17 +9,17 @@ import { @Entity() export class Like { @PrimaryGeneratedColumn() - id: number; + id!: number; @Column() - messageId: number; + messageId!: number; @Column() - channel: string; + channel!: string; @Column({ type: "text" }) - text: string; + text!: string; @Column() - author: number; + author!: number; @CreateDateColumn() - createdAt: Date; + createdAt!: Date; @UpdateDateColumn() - updatedAt: Date; + updatedAt!: Date; } diff --git a/src/service/db/postgres/entities/Post.ts b/src/service/db/postgres/entities/Post.ts index 8e4ad86..fdb2bd5 100644 --- a/src/service/db/postgres/entities/Post.ts +++ b/src/service/db/postgres/entities/Post.ts @@ -9,13 +9,13 @@ import { @Entity() export class Post { @PrimaryGeneratedColumn() - id: number; + id!: number; @Column() - eventId: number; + eventId!: number; @Column({ type: "text" }) - text: string; + text!: string; @CreateDateColumn() - createdAt: Date; + createdAt!: Date; @UpdateDateColumn() - updatedAt: Date; + updatedAt!: Date; } diff --git a/src/service/db/postgres/index.ts b/src/service/db/postgres/index.ts index 0c29372..68d0d16 100644 --- a/src/service/db/postgres/index.ts +++ b/src/service/db/postgres/index.ts @@ -11,10 +11,10 @@ import { Post } from "./entities/Post"; const entities = [path.join(__dirname, "./entities/*")]; export class PostgresDB implements Storage { - private connection: Connection; - private events: Repository; - private likes: Repository; - private posts: Repository; + private connection!: Connection; + private events!: Repository; + private likes!: Repository; + private posts!: Repository; constructor(private config: PostgresConfig) {} diff --git a/src/service/logger/index.ts b/src/service/logger/index.ts index d8e8427..ddcd1e0 100644 --- a/src/service/logger/index.ts +++ b/src/service/logger/index.ts @@ -6,7 +6,7 @@ const config = prepareConfig(); const logger = createLogger({ transports: new transports.Console({ format: format.simple(), - level: config.logger.level || "info", + level: config.logger?.level || "info", }), }); diff --git a/src/service/telegram/index.ts b/src/service/telegram/index.ts index 49fe728..159b575 100644 --- a/src/service/telegram/index.ts +++ b/src/service/telegram/index.ts @@ -40,7 +40,7 @@ export class TelegramService { if (isWebhookEnabled) { await this.bot.telegram .deleteWebhook() - .then(() => this.bot.telegram.setWebhook(this.webhook.url)) + .then(() => this.bot.telegram.setWebhook(this.webhook.url!)) .then(async () => { const info = await this.bot.telegram.getWebhookInfo(); if (!info.url) { @@ -71,7 +71,7 @@ export class TelegramService { * Checks webhook availability */ private getWebhookAvailable = async (): Promise => { - 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' return isWebhookEnabled && true; }; diff --git a/src/service/template/index.ts b/src/service/template/index.ts index 244f5f9..a847fcd 100644 --- a/src/service/template/index.ts +++ b/src/service/template/index.ts @@ -54,6 +54,7 @@ export class Template< */ public static registerHelpers() { hb.registerHelper("ifEq", function (arg1, arg2, options) { + // @ts-ignore return arg1 == arg2 ? options.fn(this) : options.inverse(this); }); } diff --git a/src/service/vk/handlers/MessageNewHandler.ts b/src/service/vk/handlers/MessageNewHandler.ts index d60be16..16e9f13 100644 --- a/src/service/vk/handlers/MessageNewHandler.ts +++ b/src/service/vk/handlers/MessageNewHandler.ts @@ -37,7 +37,7 @@ export class MessageNewHandler extends VkEventHandler { const parsed = this.template.theme({ user, group: this.group, - text: context.text, + text: context?.text || "", }); const extras: ExtraReplyMessage = { @@ -46,7 +46,7 @@ export class MessageNewHandler extends VkEventHandler { this.appendButtons(extras, user.id); - await this.telegram.sendMessageToChan(this.channel, parsed, extras); + await this.telegram.sendMessageToChan(this.channel.id, parsed, extras); await next(); }; diff --git a/src/service/vk/handlers/PostNewHandler.ts b/src/service/vk/handlers/PostNewHandler.ts index ed3730e..fa9b4bc 100644 --- a/src/service/vk/handlers/PostNewHandler.ts +++ b/src/service/vk/handlers/PostNewHandler.ts @@ -21,7 +21,7 @@ type UrlPrefix = string; type ExtraGenerator = ( text: string, eventId?: number -) => Promise; +) => Promise; interface Fields { image?: boolean; @@ -35,6 +35,7 @@ interface Values { user?: UsersUserFull; group: ConfigGroup; text: string; + type?: string; } type LikeCtx = Composer.Context & { match: string[] }; @@ -52,12 +53,9 @@ export class PostNewHandler extends VkEventHandler { public execute = async (context: WallPostContext, next: NextMiddleware) => { const id = context?.wall?.id; + const postType = context?.wall?.postType; - if ( - context.isRepost || - !PostNewHandler.isValidPostType(context?.wall?.postType) || - !id - ) { + if (context.isRepost || !this.isValidPostType(postType) || !id) { await next(); return; } @@ -75,9 +73,9 @@ export class PostNewHandler extends VkEventHandler { ? await this.getUserByID(String(context.wall.signerId)) : 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 = { disable_web_page_preview: true, @@ -95,13 +93,17 @@ export class PostNewHandler extends VkEventHandler { if (hasThumb) { const thumb = await images.find((img) => img.mediumSizeUrl); msg = await this.telegram.sendPhotoToChan( - this.channel, - this.trimTextForPhoto(text, user), - thumb.mediumSizeUrl, + this.channel.id, + this.trimTextForPhoto(text, postType, user), + thumb?.mediumSizeUrl!, extras ); } 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( @@ -109,7 +111,8 @@ export class PostNewHandler extends VkEventHandler { msg.message_id, context.wall.toJSON() ); - await this.db.createPost(event.id, context.wall.text); + + await this.db.createPost(event!.id, context?.wall?.text || ""); await next(); }; @@ -117,8 +120,16 @@ export class PostNewHandler extends VkEventHandler { /** * Checks if event of type we can handle */ - public static isValidPostType(type: string): boolean { - return type === "post"; + private isValidPostType(type?: string): boolean { + 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 { private createKeyboard = async ( text: string, eventId?: number - ): Promise => { + ): Promise => { const { buttons } = this.template.fields; if (!buttons?.length) { @@ -137,7 +148,10 @@ export class PostNewHandler extends VkEventHandler { const rows = await Promise.all( 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) { return; @@ -153,13 +167,13 @@ export class PostNewHandler extends VkEventHandler { const links = this.template.fields.links; if (!links) { - return []; + return; } const urls = extractURLs(text); if (!urls) { - return []; + return; } return urls @@ -170,7 +184,7 @@ export class PostNewHandler extends VkEventHandler { 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 { private generateLikes: ExtraGenerator = async (text, eventId) => { if (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( (acc, like) => ({ ...acc, @@ -209,7 +231,7 @@ export class PostNewHandler extends VkEventHandler { }; /** - * Adds needed listeners + * Adds needed listeners for telegram */ protected onInit = () => { if (this.template.fields.likes) { @@ -227,7 +249,7 @@ export class PostNewHandler extends VkEventHandler { * Reacts to like button press */ 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 [, channel, emo] = ctx.match; const event = await this.getEventByTgMessageId(id); @@ -237,7 +259,7 @@ export class PostNewHandler extends VkEventHandler { !emo || !id || !event || - channel != this.channel || + channel != this.channel.id || !this.likes.includes(emo) ) { await next(); @@ -261,7 +283,7 @@ export class PostNewHandler extends VkEventHandler { const markup = await this.createKeyboard(post.text, event.id); await ctx.telegram.editMessageReplyMarkup( - ctx.chat.id, + ctx.chat?.id, id, ctx.inlineMessageId, markup @@ -274,6 +296,9 @@ export class PostNewHandler extends VkEventHandler { next(); }; + /** + * Creates or updates like for {author} on {messageId} with {emo} + */ private createOrUpdateLike = async ( author: number, messageId: number, @@ -281,23 +306,31 @@ export class PostNewHandler extends VkEventHandler { ) => { return await this.db.createOrUpdateLike( messageId, - this.channel, + this.channel.id, author, emo ); }; + /** + * Gets like by {author} on {messageId} + */ 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 */ - private themeText = (text: string, user?: UsersUserFull): string => { + private themeText = ( + text: string, + type?: string, + user?: UsersUserFull + ): string => { return this.template.theme({ user, group: this.group, + type, text, }); }; @@ -305,10 +338,24 @@ export class PostNewHandler extends VkEventHandler { /** * Calculates, how much should we cut off the text to match photo caption limitations */ - private trimTextForPhoto = (text: string, user: UsersUserFull): string => { - const withText = this.themeText(text, user); - const withoutText = this.themeText("", user); + private trimTextForPhoto = ( + text: string, + type?: string, + user?: UsersUserFull + ): string => { + const withText = this.themeText(text, type, user); - return withText.slice(0, PHOTO_CAPTION_LIMIT - withoutText.length); + if (withText.length < PHOTO_CAPTION_LIMIT) { + return withText; + } + + 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); }; } diff --git a/src/service/vk/handlers/VkEventHandler.ts b/src/service/vk/handlers/VkEventHandler.ts index 3335052..853dcbe 100644 --- a/src/service/vk/handlers/VkEventHandler.ts +++ b/src/service/vk/handlers/VkEventHandler.ts @@ -1,5 +1,5 @@ import { NextMiddleware } from "middleware-io"; -import { ConfigGroup, GroupInstance, VkEvent } from "../types"; +import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types"; import { VkService } from "../index"; import { TelegramService } from "../../telegram"; import { Template } from "../../template"; @@ -13,7 +13,7 @@ export class VkEventHandler< public constructor( protected type: VkEvent, protected group: ConfigGroup, - protected channel: string, + protected channel: GroupChannel, protected instance: GroupInstance, protected vk: VkService, protected telegram: TelegramService, @@ -60,7 +60,7 @@ export class VkEventHandler< this.type, id, this.group.id, - this.channel + this.channel.id ); }; @@ -78,7 +78,7 @@ export class VkEventHandler< this.type, tgMessageId, this.group.id, - this.channel + this.channel.id ); }; @@ -94,7 +94,7 @@ export class VkEventHandler< this.type, id, this.group.id, - this.channel, + this.channel.id, tgMessageId, text ); diff --git a/src/service/vk/handlers/index.ts b/src/service/vk/handlers/index.ts index 8e7f9fa..9514ac4 100644 --- a/src/service/vk/handlers/index.ts +++ b/src/service/vk/handlers/index.ts @@ -1,4 +1,4 @@ -import { ConfigGroup, GroupInstance, VkEvent } from "../types"; +import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types"; import { VkEventHandler } from "./VkEventHandler"; import { MessageNewHandler } from "./MessageNewHandler"; import { StubHandler } from "./StubHandler"; @@ -12,7 +12,7 @@ interface Handler { new ( type: VkEvent, group: ConfigGroup, - channel: string, + channel: GroupChannel, instance: GroupInstance, vk: VkService, telegram: TelegramService, diff --git a/src/service/vk/index.ts b/src/service/vk/index.ts index 9655482..2062782 100644 --- a/src/service/vk/index.ts +++ b/src/service/vk/index.ts @@ -29,7 +29,7 @@ export class VkService { 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( (acc, group) => ({ @@ -121,7 +121,7 @@ export class VkService { const handler = new vkEventToHandler[event]( event, group, - chan.id, + chan, instance, this, this.telegram, diff --git a/src/service/vk/types.ts b/src/service/vk/types.ts index 6bbb3f6..04e78e2 100644 --- a/src/service/vk/types.ts +++ b/src/service/vk/types.ts @@ -1,4 +1,5 @@ import { API, Upload, Updates } from "vk-io"; +import { WallPostType } from "vk-io/lib/api/schemas/objects"; export interface VkConfig extends Record { groups: ConfigGroup[]; @@ -17,6 +18,7 @@ export interface ConfigGroup { export interface GroupChannel { id: string; events: VkEvent[]; + post_types: WallPostType; } export enum VkEvent { diff --git a/src/utils/extract.ts b/src/utils/extract.ts index a8044fa..5da454d 100644 --- a/src/utils/extract.ts +++ b/src/utils/extract.ts @@ -13,5 +13,5 @@ export const extractURLs = (text: string): URL[] => { return; } }) - .filter((el) => el); + .filter((el) => el) as URL[]; }; diff --git a/tsconfig.json b/tsconfig.json index f66e881..ede64be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "target": "es2017", "baseUrl": ".", "paths": {}, - "lib": ["esnext"] + "lib": ["esnext"], + "strict": true }, "include": [ "./**/*",