diff --git a/config.example.yml b/config.example.yml index ded0101..d3a9bfe 100644 --- a/config.example.yml +++ b/config.example.yml @@ -14,6 +14,7 @@ vk: groups: [] templates: message_new: templates/message_new.md + wall_post_new: templates/post_new.md # groups: # - id: 0 # name: 'Group name' diff --git a/package.json b/package.json index 5b441f8..0d8ce99 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ "remark-extract-frontmatter": "^3.1.0", "remark-frontmatter": "^3.0.0", "remark-parse-frontmatter": "^1.0.3", + "retext": "^7.0.1", "socks-proxy-agent": "^5.0.0", "telegraf": "^4.3.0", "to-vfile": "^6.1.0", "typescript": "^4.2.3", + "unist-util-filter": "^3.0.0", "url": "^0.11.0", "vk-io": "^4.2.0", "winston": "^3.3.3", diff --git a/src/service/telegram/index.ts b/src/service/telegram/index.ts index e1cc8ed..fac6483 100644 --- a/src/service/telegram/index.ts +++ b/src/service/telegram/index.ts @@ -84,6 +84,7 @@ export class TelegramService { message: string, extra?: ExtraReplyMessage ) => { + logger.debug(`sending message "${message}" to chan "${channel}"`); await this.bot.telegram.sendMessage(channel, message, extra); return; }; diff --git a/src/service/template/index.ts b/src/service/template/index.ts index 5577fc4..244f5f9 100644 --- a/src/service/template/index.ts +++ b/src/service/template/index.ts @@ -1,6 +1,6 @@ import extract from "remark-extract-frontmatter"; import frontmatter from "remark-frontmatter"; -import compiler from "remark-stringify"; +import compiler from "retext-stringify"; import parser from "remark-parse"; import unified from "unified"; import { parse } from "yaml"; @@ -8,6 +8,10 @@ import toVFile from "to-vfile"; import path from "path"; import hb from "handlebars"; +const removeFrontmatter = () => (tree) => { + tree.children = tree.children.filter((item) => item.type !== "yaml"); +}; + export class Template< F extends Record, V extends Record @@ -22,19 +26,17 @@ export class Template< } const processor = unified() - .use(parser) .use(compiler) .use(frontmatter) - .use(extract, { yaml: parse }); + .use(extract, { yaml: parse }) + .use(removeFrontmatter) + .use(parser); const file = toVFile.readSync(path.join(__dirname, "../../", filename)); const result = processor.processSync(file); this.fields = result.data as F; - this.template = result - .toString() - .replace(/^---\n(.*)---\n?$/gms, "") - .trim(); + this.template = result.contents.toString().trim(); } catch (e) { throw new Error(`Template: ${e.toString()}`); } diff --git a/src/service/vk/handlers/MessageNewHandler.ts b/src/service/vk/handlers/MessageNewHandler.ts index b750aa4..d60be16 100644 --- a/src/service/vk/handlers/MessageNewHandler.ts +++ b/src/service/vk/handlers/MessageNewHandler.ts @@ -8,7 +8,7 @@ import { ConfigGroup } from "../types"; import { ExtraReplyMessage } from "telegraf/typings/telegram-types"; interface Fields { - buttons?: string[]; + buttons?: "link"[]; link_text?: string; } @@ -46,9 +46,7 @@ export class MessageNewHandler extends VkEventHandler { this.appendButtons(extras, user.id); - await this.telegram - .sendMessageToChan(this.channel, parsed, extras) - .catch(next); + await this.telegram.sendMessageToChan(this.channel, parsed, extras); await next(); }; diff --git a/src/service/vk/handlers/PostNewHandler.ts b/src/service/vk/handlers/PostNewHandler.ts new file mode 100644 index 0000000..b5d23dd --- /dev/null +++ b/src/service/vk/handlers/PostNewHandler.ts @@ -0,0 +1,186 @@ +import { VkEventHandler } from "./VkEventHandler"; +import { WallPostContext } from "vk-io"; +import { NextMiddleware } from "middleware-io"; +import { UsersUserFull } from "vk-io/lib/api/schemas/objects"; +import { ConfigGroup } from "../types"; +import { ExtraReplyMessage } from "telegraf/typings/telegram-types"; +import { InlineKeyboardButton, Update } from "typegram"; +import { keys } from "ramda"; +import { extractURLs } from "../../../utils/extract"; +import logger from "../../logger"; +import Composer from "telegraf"; +import CallbackQueryUpdate = Update.CallbackQueryUpdate; + +type Button = "links" | "likes"; +type UrlPrefix = string; +type ExtraGenerator = (text: string) => InlineKeyboardButton[]; + +interface Fields { + image?: boolean; + buttons?: Button[]; + link_text?: string; + links: Record; + likes?: string[]; +} + +interface Values { + user?: UsersUserFull; + group: ConfigGroup; + text: string; +} + +type LikeCtx = Composer.Context & { match: string[] }; + +export class PostNewHandler extends VkEventHandler { + constructor(...props: any) { + // @ts-ignore + super(...props); + this.onInit(); + } + + private likes: string[] = ["👎", "👍"]; + + public execute = async (context: WallPostContext, next: NextMiddleware) => { + if ( + context.isRepost || + !PostNewHandler.isValidPostType(context?.wall?.postType) + ) { + await next(); + return; + } + + const user = context.wall.signerId + ? await this.getUserByID(String(context.wall.signerId)) + : undefined; + + const text = context.wall.text.trim(); + + const parsed = this.template.theme({ + user, + group: this.group, + text, + }); + + const extras: ExtraReplyMessage = { + disable_web_page_preview: true, + }; + + this.appendExtras(extras, text); + + await this.telegram.sendMessageToChan(this.channel, parsed, extras); + + await next(); + }; + + /** + * Checks if event of type we can handle + */ + public static isValidPostType(type: string): boolean { + return type === "post"; + } + + /** + * Creates extras + */ + private appendExtras = (extras: ExtraReplyMessage, text: string) => { + const { buttons } = this.template.fields; + if (!buttons?.length) { + return; + } + + const keyboard = buttons + .map((button) => this.extrasGenerators[button](text)) + .filter((el) => el && el.length); + + if (!keyboard.length) { + return; + } + + extras.reply_markup = { + inline_keyboard: keyboard, + }; + }; + + /** + * Generates link buttons for post + */ + private generateLinks: ExtraGenerator = (text) => { + const links = this.template.fields.links; + + if (!links) { + return []; + } + + const urls = extractURLs(text); + + if (!urls) { + return []; + } + + return urls + .map((url) => { + const label = keys(links).find((link) => + url.toString().startsWith(link) + ); + + return label ? { text: links[label], url: url.toString() } : undefined; + }) + .filter((el) => el); + }; + + /** + * Generates like button + */ + private generateLikes: ExtraGenerator = () => { + return this.likes.map((like, i) => ({ + text: like, + callback_data: `/like ${this.channel} ${like}`, + })); + }; + + /** + * Button generators dictionary + */ + private extrasGenerators: Record = { + links: this.generateLinks, + likes: this.generateLikes, + }; + + /** + * Adds needed listeners + */ + protected onInit = () => { + if (this.template.fields.likes) { + this.likes = this.template.fields.likes; + } + + if (!this.template.fields.buttons?.includes("likes")) { + return; + } + + this.telegram.bot.action(/like (.*) (.*)/, this.onLikeAction); + }; + + /** + * Reacts to like button press + */ + onLikeAction = async (ctx: LikeCtx, next) => { + const id = ctx.update.callback_query.message.message_id; + const [_, channel, emo] = ctx.match; + + if ( + !channel || + !emo || + !id || + channel != this.channel || + !this.likes.includes(emo) + ) { + await next(); + return; + } + + logger.warn( + `someone reacted with ${emo} to message ${id} on channel ${channel}` + ); + }; +} diff --git a/src/service/vk/handlers/index.ts b/src/service/vk/handlers/index.ts index 38af5b6..f1763cd 100644 --- a/src/service/vk/handlers/index.ts +++ b/src/service/vk/handlers/index.ts @@ -5,6 +5,7 @@ import { StubHandler } from "./StubHandler"; import { VkService } from "../index"; import { TelegramService } from "../../telegram"; import { Template } from "../../template"; +import { PostNewHandler } from "./PostNewHandler"; interface Handler { new ( @@ -22,6 +23,5 @@ export const vkEventToHandler: Record = { [VkEvent.GroupJoin]: StubHandler, [VkEvent.GroupLeave]: StubHandler, [VkEvent.MessageNew]: MessageNewHandler, - [VkEvent.PostSuggestion]: StubHandler, - [VkEvent.WallPostNew]: StubHandler, + [VkEvent.WallPostNew]: PostNewHandler, }; diff --git a/src/service/vk/types.ts b/src/service/vk/types.ts index 62ee3e8..374b5af 100644 --- a/src/service/vk/types.ts +++ b/src/service/vk/types.ts @@ -21,7 +21,6 @@ interface GroupChannel { export enum VkEvent { WallPostNew = "wall_post_new", - PostSuggestion = "post_suggestion", GroupJoin = "group_join", GroupLeave = "group_leave", MessageNew = "message_new", diff --git a/src/utils/extract.ts b/src/utils/extract.ts new file mode 100644 index 0000000..a8044fa --- /dev/null +++ b/src/utils/extract.ts @@ -0,0 +1,17 @@ +import { URL } from "url"; + +const urlRe = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gim; + +export const extractURLs = (text: string): URL[] => { + const matches = text.match(urlRe) || []; + + return matches + .map((m) => { + try { + return new URL(m); + } catch (e) { + return; + } + }) + .filter((el) => el); +}; diff --git a/templates/post_new.md b/templates/post_new.md new file mode 100644 index 0000000..a4df2df --- /dev/null +++ b/templates/post_new.md @@ -0,0 +1,21 @@ +--- + image: true + buttons: [likes,links] + link_text: Пост полностью + links: + https://map.vault48.org/: Посмотреть карту + http://map.vault48.org/: Посмотреть карту + https://vk.com/album-: Альбом поката + http://vk.com/album-: Альбом поката + likes: ['😱','🤔','😃'] +--- +{{!-- + use handlebars template here + available variables are: text, user, group + (see PostNewHandler) +--}} +{{text}} + +{{#if user}} +[{{user.first_name}} {{user.last_name}}](https://vk.com/id{{user.id}}) +{{/if}} diff --git a/yarn.lock b/yarn.lock index ebf30c7..188f086 100644 --- a/yarn.lock +++ b/yarn.lock @@ -191,6 +191,11 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array-iterate@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array-iterate/-/array-iterate-1.1.4.tgz#add1522e9dd9749bb41152d08b845bd08d6af8b7" + integrity sha512-sNRaPGh9nnmdC8Zf+pT3UqP8rnWj5Hf9wiFGsX3wUQ2yVSIhO2ShFwCoceIPpB41QF6i2OEmrHmCo36xronCVA== + async@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" @@ -661,7 +666,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -955,6 +960,11 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +nlcst-to-string@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/nlcst-to-string/-/nlcst-to-string-2.0.4.tgz#9315dfab80882bbfd86ddf1b706f53622dc400cc" + integrity sha512-3x3jwTd6UPG7vi5k4GEzvxJ5rDA7hVUIRNHPblKuMVP9Z3xmlsd9cgLcpAMkc5uPOBna82EeshROFhsPkbnTZg== + node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -1027,6 +1037,15 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-latin@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/parse-latin/-/parse-latin-4.3.0.tgz#1a70fc5601743baa06c5f12253c334fc94b4a917" + integrity sha512-TYKL+K98dcAWoCw/Ac1yrPviU8Trk+/gmjQVaoWEFDZmVD4KRg6c/80xKqNNFQObo2mTONgF8trzAf2UTwKafw== + dependencies: + nlcst-to-string "^2.0.0" + unist-util-modify-children "^2.0.0" + unist-util-visit-children "^1.0.0" + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -1205,6 +1224,30 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +retext-latin@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/retext-latin/-/retext-latin-2.0.4.tgz#ef5d34ae7641ae56b0675ea391095e8ee762b251" + integrity sha512-fOoSSoQgDZ+l/uS81oxI3alBghDUPja0JEl0TpQxI6MN+dhM6fLFumPJwMZ4PJTyL5FFAgjlsdv8IX+6IRuwMw== + dependencies: + parse-latin "^4.0.0" + unherit "^1.0.4" + +retext-stringify@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/retext-stringify/-/retext-stringify-2.0.4.tgz#496d6c532f7dc6d15e4b262de0266e828f72efa9" + integrity sha512-xOtx5mFJBoT3j7PBtiY2I+mEGERNniofWktI1cKXvjMEJPOuqve0dghLHO1+gz/gScLn4zqspDGv4kk2wS5kSA== + dependencies: + nlcst-to-string "^2.0.0" + +retext@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/retext/-/retext-7.0.1.tgz#04b7965ab78fe6e5e3a489304545b460d41bf5aa" + integrity sha512-N0IaEDkvUjqyfn3/gwxVfI51IxfGzOiVXqPLWnKeCDbiQdxSg0zebzHPxXWnL7TeplAJ+RE4uqrXyYN//s9HjQ== + dependencies: + retext-latin "^2.0.0" + retext-stringify "^2.0.0" + unified "^8.0.0" + revalidator@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.3.1.tgz#ff2cc4cf7cc7c6385ac710178276e6dbcd03762f" @@ -1439,6 +1482,25 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.4.tgz#592588bb9f47ae03b24916e2471218d914955574" integrity sha512-kv7fCkIXyQIilD5/yQy8O+uagsYIOt5cZvs890W40/e/rvjMSzJw81o9Bg0tkURxzZBROtDQhW2LFjOGoK3RZw== +unherit@^1.0.4: + version "1.1.3" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" + integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== + dependencies: + inherits "^2.0.0" + xtend "^4.0.0" + +unified@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-8.4.2.tgz#13ad58b4a437faa2751a4a4c6a16f680c500fff1" + integrity sha512-JCrmN13jI4+h9UAyKEoGcDZV+i1E7BLFuG7OsaDvTXI5P0qhHX+vZO/kOhz9jn8HGENDKbwSeB0nVOg4gVStGA== + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-plain-obj "^2.0.0" + trough "^1.0.0" + vfile "^4.0.0" + unified@^9.1.0: version "9.2.1" resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.1.tgz#ae18d5674c114021bfdbdf73865ca60f410215a3" @@ -1451,6 +1513,14 @@ unified@^9.1.0: trough "^1.0.0" vfile "^4.0.0" +unist-util-filter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unist-util-filter/-/unist-util-filter-3.0.0.tgz#aae33b1dff78875dfe37351ad3363478ac0f110e" + integrity sha512-Vxzp2Z2LJuJLEkXYdfuW+fwWzC5xz1sjDFL3+xyGaDMxsn0pB457oQ8gdDbuhQdUSdxtuJ/WF7gDQVHOh7kyCg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-find@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unist-util-find/-/unist-util-find-1.0.2.tgz#4d5b01a69fca2a382ad4f55f9865e402129ecf56" @@ -1464,6 +1534,18 @@ unist-util-is@^3.0.0: resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A== +unist-util-is@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.0.0.tgz#c71eddea34aa7009d54f671a6fafb3779b8035d3" + integrity sha512-G4p13DhfdUNmlnJxd0uy5Skx1FG58LSDhX8h1xgpeSq0omOQ4ZN5BO54ToFlNX55NDTbRHMdwTOJXqAieInSEA== + +unist-util-modify-children@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-2.0.0.tgz#9c9c30d4e32502aabb3fde10d7872a17c86801e2" + integrity sha512-HGrj7JQo9DwZt8XFsX8UD4gGqOsIlCih9opG6Y+N11XqkBGKzHo8cvDi+MfQQgiZ7zXRUiQREYHhjOBHERTMdg== + dependencies: + array-iterate "^1.0.0" + unist-util-stringify-position@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" @@ -1471,6 +1553,11 @@ unist-util-stringify-position@^2.0.0: dependencies: "@types/unist" "^2.0.2" +unist-util-visit-children@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/unist-util-visit-children/-/unist-util-visit-children-1.1.4.tgz#e8a087e58a33a2815f76ea1901c15dec2cb4b432" + integrity sha512-sA/nXwYRCQVRwZU2/tQWUqJ9JSFM1X3x7JIOsIgSzrFHcfVt6NkzDtKzyxg2cZWkCwGF9CO8x4QNZRJRMK8FeQ== + unist-util-visit-parents@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9" @@ -1597,7 +1684,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==