diff --git a/Procfile b/Procfile index f2d1f0b..9ebe8e8 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: node index.js +worker: npm start diff --git a/README.md b/README.md index 460aca1..9c8d671 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@

Перед началом работы настройте config.json! +
+ Скрипт запускается командой npm start!

CodeFactor

@@ -19,7 +21,11 @@ "longpoll": false // Использовать Longpoll API. true = Вкл. / false = Выкл. }, "discord": { - "webhook_url": "https://discordapp.com/api/webhooks/", // Ваш WebHook URL. + "webhook_urls": [ + "https://discordapp.com/api/webhooks/", + "https://discordapp.com/api/webhooks/", + ... + ], // Ссылки на Webhook, можно использовать несколько ссылок на разные каналы Discord. "bot_name": "VK2DISCORD", // Имя вашего WebHook, выcвечиваетеся в качестве имени бота. "color": "#aabbcc" // Цвет рамки сообщения Discord в формате HEX. }, diff --git a/config.json b/config.json index 24916a3..df5054c 100644 --- a/config.json +++ b/config.json @@ -7,9 +7,12 @@ "longpoll": false }, "discord": { - "webhook_url": "https://discordapp.com/api/webhooks/", + "webhook_urls": [ + "https://discordapp.com/api/webhooks/", + "https://discordapp.com/api/webhooks/" + ], "bot_name": "VK2DISCORD", "color": "#aabbcc" }, "interval": 30 -} +} \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 885e4d7..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -require("./modules/handler"); \ No newline at end of file diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..7b1634f --- /dev/null +++ b/index.mjs @@ -0,0 +1 @@ +import "./modules/handler"; \ No newline at end of file diff --git a/modules/discord.mjs b/modules/discord.mjs new file mode 100644 index 0000000..788f2fa --- /dev/null +++ b/modules/discord.mjs @@ -0,0 +1,16 @@ +import webhook from "webhook-discord"; + +import config from "../config"; + +const name = config.discord.bot_name.slice(0, 32); +const color = config.discord.color.match(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/m) ? config.discord.color : "#aabbcc"; +const urls = config.discord.webhook_urls; +/*const sendAll = config.discord.send_all;*/ + +export { + webhook, + name, + color, + urls, + /*sendAll*/ +}; \ No newline at end of file diff --git a/modules/functions.js b/modules/functions.js deleted file mode 100644 index 1b24332..0000000 --- a/modules/functions.js +++ /dev/null @@ -1,60 +0,0 @@ -function parseLinks(text) { - return `${text.replace(/(?:\[([^]+?)\|([^]+?)])/g, "[$2](https://vk.com/$1)")}\n\n`; -} - -function checkKeywords(keywords, text) { - if (keywords.length > 0) { - return keywords.some(keyword => { - return text.match(keyword, "gi"); - }); - } else { - return true; - } -} - -async function getAttachments(attachments, webhookBuilder, longpoll) { - let text = ""; - - await attachments.forEach(item => { - const type = item.type; - - switch (type) { - case "photo": - if (!webhookBuilder.data.attachments[0].image_url) webhookBuilder.setImage((longpoll ? item.sizes : item.photo.sizes).pop().url); - break; - case "video": - text += `\n[:video_camera: Смотреть видео: ${(longpoll ? item : item.video).title}](https://vk.com/video${longpoll ? item.ownerId : item.video.owner_id}_${(longpoll ? item : item.video).id})`; - break; - case "link": - text += `\n[:link: ${(longpoll ? item : item.link).button_text || "Ссылка"}: ${(longpoll ? item : item.link).title}](${(longpoll ? item : item.link).url})`; - break; - case "doc": - text += `\n[:page_facing_up: Документ: ${(longpoll ? item : item.doc).title}](${(longpoll ? item : item.doc).url})`; - break; - case "audio": - const artist = (longpoll ? item : item.audio).artist; - const title = (longpoll ? item : item.audio).title; - - text += `\n[:musical_note: Музыка: ${artist} - ${title}](https://vk.com/search?c[section]=audio&c[q]=${encodeURI(artist.replace(/&/g, "и"))}%20-%20${encodeURI(title)}&c[performer]=1)`; - break; - case "poll": - let answers = ""; - - (longpoll ? item : item.poll).answers.forEach(item => answers += `\n• ${item.text}`); - text += `\n[:bar_chart: Опрос: ${(longpoll ? item : item.poll).question}](https://vk.com/feed?w=poll${longpoll ? item.ownerId : item.poll.owner_id}_${(longpoll ? item : item.poll).id})`; - break; - } - }); - return text; -} - -function errorHandler(error) { - console.log(`[!] Возникла ошибка: ${error}. Если не понимаете в чем причина, свяжитесь со мной: https://vk.com/id233731786`); -} - -module.exports = { - parseLinks, - checkKeywords, - getAttachments, - errorHandler -}; diff --git a/modules/handler.js b/modules/handler.js deleted file mode 100644 index 01dd6d1..0000000 --- a/modules/handler.js +++ /dev/null @@ -1,75 +0,0 @@ -const { VK } = require("vk-io"); -const config = require("../config"); -const webhook = require("webhook-discord"); - -const vk = new VK(); -const { updates, api } = vk; - -const token = config.vk.token; -const longpoll = config.vk.longpoll; -const groupId = config.vk.group_id; -const interval = config.interval * 1000; - -const send = require("./send"); - -const { errorHandler } = require("./functions"); - -vk.setOptions({ - token, - apiMode: "parallel" -}); - - -if (!longpoll) { - if (interval < 30000) console.log("[!] Не рекомендуем ставить интервал получения постов меньше 30 секунд, во избежания лимитов ВКонтакте!"); - - setInterval(() => { - const webhookBuilder = new webhook.MessageBuilder(); - - const groupIdMatch = groupId.match(/^(?:public|group)([\d]+)$/); - const userIdMatch = groupId.match(/^id([\d]+)$/); - const id = groupIdMatch ? {owner_id: -groupIdMatch[1]} : userIdMatch ? {owner_id: userIdMatch[1]} : {domain: groupId}; - - api.wall.get({ - ...id, - count: 2, - extended: 1, - filter: config.vk.filter ? "owner" : "all", - v: "5.103" - }) - .then(data => { - - if (data.groups.length > 0 && groupIdMatch) { - webhookBuilder.setFooter(data.groups[0].name, data.groups[0].photo_50); - } else if (data.profiles.length > 0) { - webhookBuilder.setFooter(`${data.profiles[0].first_name} ${data.profiles[0].last_name}`, data.profiles[0].photo_50); - } - - const posts = data.items; - const post1 = posts[0]; - const post2 = posts[1]; - - if (posts.length > 0) { - const postData = posts.length === 2 && post2.date > post1.date ? post2 : post1; - - send(webhookBuilder, postData, false); - } else { - console.log("[!] Не получено ни одной записи. Проверьте наличие записей в группе или измените значение фильтра в конфигурации."); - } - - }) - .catch(err => errorHandler(err)); - }, interval); -} else { - updates.on("new_wall_post", context => { - const webhookBuilder = new webhook.MessageBuilder(); - - send(webhookBuilder, context.wall, true); - }); - - updates.start() - .then(() => console.log("[Бот] Подключен к ВКонтакте!")) - .catch(err => errorHandler(err)); -} - -console.log("[Бот] Запущен"); diff --git a/modules/handler.mjs b/modules/handler.mjs new file mode 100644 index 0000000..710c22b --- /dev/null +++ b/modules/handler.mjs @@ -0,0 +1,80 @@ +import { updates, api, longpoll, groupId, interval, filter } from "./vk"; + +import { webhook } from "./discord"; + +import { Sender } from "./sender"; + +if (!longpoll) { + if (interval < 30000) console.log("[!] Не рекомендуем ставить интервал получения постов меньше 30 секунд, во избежания лимитов ВКонтакте!"); + + setInterval(() => { + const sender = new Sender(); + + const groupIdMatch = groupId.match(/^(?:public|group)([\d]+)$/); + const userIdMatch = groupId.match(/^id([\d]+)$/); + const id = groupIdMatch ? + { + owner_id: -groupIdMatch[1] + } + : + userIdMatch ? + { + owner_id: userIdMatch[1] + } + : + { + domain: groupId + }; + + api.wall.get({ + ...id, + count: 2, + extended: 1, + filter: filter ? "owner" : "all", + v: "5.103" + }) + .then(data => { + const builder = new webhook.MessageBuilder(); + + if (data.groups.length > 0 && groupIdMatch) { // Устанавливаем footer от типа отправителя записи + builder.setFooter(data.groups[0].name, data.groups[0].photo_50); + } else if (data.profiles.length > 0) { + builder.setFooter(`${data.profiles[0].first_name} ${data.profiles[0].last_name}`, data.profiles[0].photo_50); + } + + const posts = data.items; // Проверяем наличие закрепа, если он есть берем свежую запись + const post1 = posts[0]; + const post2 = posts[1]; + + if (posts.length > 0) { + const postData = posts.length === 2 && post2.date > post1.date ? post2 : post1; + + sender.Post(builder, postData); + } else { + console.log("[!] Не получено ни одной записи. Проверьте наличие записей в группе или измените значение фильтра в конфигурации."); + } + + }) + .catch(err => errorHandler(err)); + }, interval); +} else { + updates.on("new_wall_post", context => { + const sender = new Sender(); + + sender.Post(new webhook.MessageBuilder(), context.wall) + }); + + updates.start() + .then(() => console.log("[VK2DISCORD] Подключен к ВКонтакте!")) + .catch(err => errorHandler(err)); +} + +console.log("[VK2DISCORD] Запущен."); + +function errorHandler(error) { + console.log(`[!] Возникла ошибка: ${error}. Если не понимаете в чем причина, свяжитесь со мной: https://vk.com/id233731786`); +} + +export { + errorHandler +}; diff --git a/modules/send.js b/modules/send.js deleted file mode 100644 index e93fc92..0000000 --- a/modules/send.js +++ /dev/null @@ -1,75 +0,0 @@ -const fs = require("fs"); -const webhook = require("webhook-discord"); - -const news = require("../news"); -const config = require("../config"); - -const { getAttachments, parseLinks, checkKeywords, errorHandler } = require("./functions"); - -const keywords = config.vk.keywords; -const name = config.discord.bot_name; -const color = config.discord.color.match(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/m) ? config.discord.color : "#aabbcc"; -const url = config.discord.webhook_url; - -const discord = new webhook.Webhook(url); - -module.exports = async (webhookBuilder, postData, longpoll) => { - webhookBuilder.setName(name.slice(0, 32)) - .setColor(color); - - const createdAt = longpoll ? postData.createdAt : postData.date; - - if (news.last_post !== createdAt && !(news.published_posts.includes(createdAt)) && checkKeywords(keywords, postData.text)) { - if (longpoll && config.vk.filter && postData.authorId === postData.createdUserId) return; - - let postText = `[**Открыть пост ВКонтакте**](https://vk.com/wall${longpoll ? postData.authorId : postData.from_id}_${postData.id})\n\n`; - - if (postData.text) postText += parseLinks(postData.text); - - let attachments = ""; - if (postData.attachments) attachments += await getAttachments(postData.attachments, webhookBuilder, longpoll); - - const repost = longpoll ? - postData.copyHistory ? postData.copyHistory[0] : null - : - postData.copy_history ? postData.copy_history[0] : null; - - let repostText = ""; - let reportAttachments = ""; - if (repost) { - repostText += `\n>>> [**Репост записи**](https://vk.com/wall${repost.from_id}_${repost.id})\n\n`; - if (repost.text) { - repostText += parseLinks(repost.text); - } - - if (repost.attachments) { - reportAttachments += await getAttachments(repost.attachments, webhookBuilder, longpoll); - } - } - - const allPost = postText + attachments + repostText + reportAttachments; - - webhookBuilder.setDescription(allPost.length > 2048 ? - (postText ? postText.slice(0, repostText ? 1021 - attachments.length : 2045 - attachments.length) + (postData.text ? "…\n\n" : "") : "") - + attachments - + (repostText ? repostText.slice(0, postText ? 1021 - reportAttachments.length : 2045 - reportAttachments.length) + (repost.text ? "…\n\n" : "") : "") - + reportAttachments - : - allPost); - - discord.send(webhookBuilder) - .then(() => { - console.log("[!] Пост успешно опубликован в Discord канале."); - - news.last_post = createdAt; - news.published_posts.unshift(createdAt); - - if (news.published_posts.length >= 30) news.published_posts.splice(-1, 1); - - fs.writeFileSync("./news.json", JSON.stringify(news, null, "\t")); - }) - .catch(err => errorHandler(err)); - } else { - console.log("[!] Новых записей нет или они не соответствуют ключевым словам!"); - } -}; diff --git a/modules/sender.mjs b/modules/sender.mjs new file mode 100644 index 0000000..908566f --- /dev/null +++ b/modules/sender.mjs @@ -0,0 +1,177 @@ +import fs from "fs"; + +import { webhook, name, color, urls/*, sendAll*/ } from "./discord"; +import { keywords, longpoll, filter } from "./vk"; + +import { errorHandler } from "./handler"; + +import news from "../news"; + +export class Sender { + state = { + post: { + text: "", + attachments: "" + }, + repost: { + text: "", + attachments: "" + }, + webhookBuilders: [] + }; + + async Post(builder, postData) { + const { post, repost, webhookBuilders } = this.state; + + webhookBuilders.push(builder); + + const createdAt = longpoll ? postData.createdAt : postData.date; + + if (news.last_post !== createdAt && !(news.published_posts.includes(createdAt)) && this.CheckKeywords(postData.text)) { // Проверяем что пост не был опубликован и соответствует ключевым словам + + if (longpoll && filter && postData.authorId === postData.createdUserId) return; // Фильтр на записи только от имени группы для LongPoll + + post.text += `[**Открыть пост ВКонтакте**](https://vk.com/wall${longpoll ? postData.authorId : postData.from_id}_${postData.id})\n\n`; + + if (postData.text) post.text += this.FixLinks(postData.text); + + if (postData.attachments) post.attachments += await this.ParseAttachments(postData.attachments); + + const Repost = longpoll ? + postData.copyHistory ? postData.copyHistory[0] : null + : + postData.copy_history ? postData.copy_history[0] : null; + + if (Repost) { + repost.text += `\n\n>>> [**Репост записи**](https://vk.com/wall${longpoll ? Repost.authorId : Repost.from_id}_${Repost.id})\n\n`; + + if (Repost.text) repost.text += this.FixLinks(Repost.text); + + if (Repost.attachments) repost.attachments += await this.ParseAttachments(Repost.attachments); + } + + this.Send(createdAt); + } else { + console.log("[!] Новых записей нет или они не соответствуют ключевым словам!"); + } + } + + async ParseAttachments(attachments) { + const { webhookBuilders } = this.state; + const builder = webhookBuilders[0]; + + let text = ""; + + await attachments.forEach(item => { + const type = item.type; + + switch (type) { + case "photo": + if (!builder.data.attachments[0].image_url) { + builder.setImage((longpoll ? item.sizes : item.photo.sizes).pop().url); + } + break; + case "video": + const video = longpoll ? item : item.video; + + text += `\n[:video_camera: Смотреть видео: ${video.title}](https://vk.com/video${longpoll ? video.ownerId : video.owner_id}_${video.id})`; + break; + case "link": + const link = longpoll ? item : item.link; + + text += `\n[:link: ${link.button_text || "Ссылка"}: ${link.title}](${link.url})`; + break; + case "doc": + const doc = longpoll ? item : item.doc; + const ext = longpoll ? doc.typeName : doc.ext; + + if (ext === "gif") { + builder.setImage(doc.url); + } else { + text += `\n[:page_facing_up: Документ: ${doc.title}](${doc.url})`; + } + break; + case "audio": + const audio = longpoll ? item : item.audio; + + const artist = audio.artist; + const title = audio.title; + + text += `\n[:musical_note: Музыка: ${artist} - ${title}](https://vk.com/search?c[section]=audio&c[q]=${encodeURI(artist.replace(/&/g, "и"))}%20-%20${encodeURI(title)}&c[performer]=1)`; + break; + case "poll": + const poll = longpoll ? item : item.poll; + + text += `\n[:bar_chart: Опрос: ${poll.question}](https://vk.com/feed?w=poll${longpoll ? poll.ownerId : poll.owner_id}_${poll.id})`; + break; + } + }); + + /*if (sendAll) { + await this.ParsePhotos(attachments); + }*/ + + return text; + } + + /*async ParsePhotos(attachments) { + const { webhookBuilders } = this.state; + + const photos = attachments.filter(attachment => attachment.type === "photo"); + + await photos.forEach((item, index) => { + const builder = new webhook.MessageBuilder(); + + if (photos.length > 1 && index === 0) return; + + builder.setImage((longpoll ? item.sizes : item.photo.sizes).pop().url); + + webhookBuilders.push(builder); + }); + }*/ + + CheckKeywords(text) { + if (keywords.length > 0) { + return keywords.some(keyword => { + return text.match(keyword, "gi"); + }); + } else { + return true; + } + } + + FixLinks(text) { + return decodeURI(`${text.replace(/(?:\[(https:\/\/vk.com\/[^]+?)\|([^]+?)])/g, "[$2]($1)").replace(/(?:\[([^]+?)\|([^]+?)])/g, "[$2](https://vk.com/$1)")}\n\n`); + } + + async Send(createdAt) { + const { post, repost, webhookBuilders } = this.state; + const builder = webhookBuilders[0].setName(name).setColor(color); + + if (post.text.length + post.attachments.length + repost.text.length + repost.attachments.length > 2048) { + if (post.text) { + post.text = post.text.slice(0, repost.text ? 1021 - post.attachments.length : 2045 - post.attachments.length) + "…\n\n" + } + + if (repost.text) { + repost.text = repost.text.slice(0, post.text ? 1021 - repost.attachments.length : 2045 - repost.attachments.length) + "…\n\n" + } + } + + builder.setDescription(post.text + post.attachments + repost.text + repost.attachments); + + urls.forEach((url, index) => { + new webhook.Webhook(url) + .send(builder) + .then(console.log(`[!] Пост успешно опубликован в Discord-канале #${index + 1}.`)) + .catch((error) => errorHandler(error)); + }); + + news.last_post = createdAt; + news.published_posts.unshift(createdAt); + + if (news.published_posts.length >= 30) news.published_posts.splice(-1, 1); + + fs.writeFileSync("./news.json", JSON.stringify(news, null, "\t")); + } +} \ No newline at end of file diff --git a/modules/vk.mjs b/modules/vk.mjs new file mode 100644 index 0000000..5fae629 --- /dev/null +++ b/modules/vk.mjs @@ -0,0 +1,34 @@ +import config from "../config"; +import VKIO from "vk-io"; + +const { VK } = VKIO; +const vk = new VK(); + +const token = config.vk.token; +const groupId = config.vk.group_id; +const longpoll = config.vk.longpoll; + +const interval = config.interval * 1000; + +const filter = config.vk.filter; +const keywords = config.vk.keywords; + +vk.setOptions({ + token, + apiMode: "parallel" // Необходимо исользовать LongPoll версии 5.103 +}); + +const { updates, api } = vk; + +export { + updates, + api, + + longpoll, + groupId, + + interval, + + filter, + keywords +}; \ No newline at end of file diff --git a/news.json b/news.json index 19dc713..bbad8b1 100644 --- a/news.json +++ b/news.json @@ -1,4 +1,4 @@ { - "last_post": null, + "last_post": 0, "published_posts": [] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 57009f9..a446853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,17 @@ { "name": "vk2discord", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, "fs": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", @@ -14,6 +22,11 @@ "resolved": "https://registry.npmjs.org/middleware-io/-/middleware-io-2.4.0.tgz", "integrity": "sha512-pzK7Eg5ZlDesrgThEce4jhl0TJfU6ZTH8TZRKUSnWiN2yMmTSw6/EHpqQOm/g/YzkE83yH9rC3PqN+oPHQox5w==" }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", diff --git a/package.json b/package.json index 16cae07..b8220e3 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,19 @@ { "name": "vk2discord", - "version": "1.5.0", + "version": "1.6.0", "description": "Автоматическая публикация постов из VK.COM в канал Discord", - "main": "index.js", + "main": "index.mjs", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node --experimental-modules --experimental-json-modules --es-module-specifier-resolution=node index.mjs" }, "repository": { "type": "git", "url": "git+https://github.com/MrZillaGold/VK2Discord.git" }, - "author": "MrZillaGold", + "author": { + "name": "MrZillaGold", + "url": "https://vk.com/mrzillagold" + }, "license": "SEE LICENSE IN LICENSE.txt", "bugs": { "url": "https://github.com/MrZillaGold/VK2Discord/issues"