1
0
Fork 0
mirror of https://github.com/muerwre/vk-tg-bot.git synced 2025-04-28 16:36:40 +07:00

Compare commits

..

No commits in common. "eb63618aeb35ee3112bc78d6bfc75ae77a21b709" and "969103d111b9eb6ac7de14bf05e33e256b3ccd60" have entirely different histories.

14 changed files with 37 additions and 197 deletions

View file

@ -1,15 +1,12 @@
# Vk to Telegram bot
### Configuring
Copy `config.example.yml` to `config.yml` and set it up.
#### Running
Setup environment: `yarn environment`, then run the application: `yarn && yarn build && node ./dist/index.js`
`yarn && yarn build && node ./dist/index.js`
#### Custom arguments
```bash
node ./dist/index.js \
--config ./config-dev.yml

View file

@ -10,15 +10,8 @@ RUN yarn
COPY . .
RUN yarn build
FROM node:18-bookworm AS runner
WORKDIR /app/dist
COPY --from=builder /app/dist ./
COPY --from=builder /app/templates /templates
COPY ./docker/wait-for-it.sh .
EXPOSE 80
CMD ["node", "./index.js"]

View file

@ -5,10 +5,9 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"environment": "docker-compose -f ./docker/docker-compose.yml up db -d",
"start": "node ./dist/index.js",
"dev": "NODE_ENV=dev node -r ts-node/register ./src/index.ts --config=./config.yml",
"build": "rm -rf ./output ./dist && tsc && yarn ncc build ./output/index.js -o ./dist && rm -rf ./output",
"build": "rm -rf ./dist && tsc && copyfiles -f ./config*.yml ./dist && copyfiles ./templates/*.md ./dist",
"test": "jest"
},
"dependencies": {
@ -56,7 +55,6 @@
"@types/winston": "^2.4.4",
"@types/yargs": "^16.0.1",
"@types/yup": "^0.29.11",
"@vercel/ncc": "^0.38.3",
"copyfiles": "^2.4.1",
"jest": "^29.7.0",
"prettier": "^2.2.1",

View file

@ -30,11 +30,8 @@ export default function prepareConfig() {
const key = JSON.parse(
fs.readFileSync(config.calendar?.keyFile).toString()
) as CalendarKeyFile;
if (key) {
calendarKeyValidator.validateSync(key);
config.calendarKey = key;
}
calendarKeyValidator.validateSync(key);
config.calendarKey = key;
} catch (error) {
console.warn("tried to parse calendar key, got error", error);
}

View file

@ -9,7 +9,7 @@ import path from "path";
import hb from "handlebars";
import strip from "strip-markdown";
import { VFileCompatible } from "vfile";
import { transformMDLinks } from "../../utils/links";
import transformMDLinks from "../../utils/transformMDLinks";
const removeFrontmatter = () => (tree) => {
tree.children = tree.children.filter((item) => item.type !== "yaml");

View file

@ -11,7 +11,7 @@ import {
User,
} from "typegram";
import { keys } from "lodash";
import { extractURLs } from "../../../utils/links";
import { extractURLs } from "../../../utils/extract";
import logger from "../../logger";
import Composer from "telegraf";
import { Template } from "../../template";

View file

@ -1,46 +0,0 @@
import { extractURLs } from "../links";
describe("extractURLs", () => {
it("extracts simple urls", () => {
const result = extractURLs(
"Trying out links https://map.vault48.org/test 123"
);
expect(result.length).toBe(1);
expect(result[0].href).toBe("https://map.vault48.org/test");
});
it("works with that weird new VK urls", () => {
const result = extractURLs(
"Trying out links: [#alias|map.vault48.org/test|https://map.vault48.org/test]"
);
expect(result.length).toBe(1);
expect(result[0].href).toBe("https://map.vault48.org/test");
});
it("works with that weird new VK urls without scheme", () => {
const result = extractURLs(
"Trying out links: [#alias|map.vault48.org/test|map.vault48.org/test]"
);
expect(result.length).toBe(1);
expect(result[0].href).toBe("https://map.vault48.org/test");
});
it("deduplicates matching urls", () => {
const result = extractURLs(
`Trying out links: [#alias|map.vault48.org/test|map.vault48.org/test] map.vault48.org/test https://map.vault48.org/test map.vault48.org/test2 https://map.vault48.org/test3
[#alias|map.vault48.org/test2|map.vault48.org/test2] [#alias|map.vault48.org/test3|map.vault48.org/test3] [#alias|map.vault48.org/test4|map.vault48.org/test4] https://map.vault48.org/test5
`
).map((it) => it.href);
expect(result).toEqual([
"https://map.vault48.org/test",
"https://map.vault48.org/test2",
"https://map.vault48.org/test3",
"https://map.vault48.org/test4",
"https://map.vault48.org/test5",
]);
});
});

View file

@ -1,29 +0,0 @@
import { transformMDLinks } from "../links";
describe("transformMDLinks", () => {
it("extracts simple urls", () => {
expect(
transformMDLinks("Trying out links https://map.vault48.org/test 123")
).toBe(
"Trying out links [https://map.vault48…](https://map.vault48.org/test) 123"
);
});
it("works with that weird new VK urls", () => {
expect(
transformMDLinks(
"Trying out links [#alias|12345678901234567890123|https://map.vault48.org/test_abc_def_ghi] 123"
)
).toBe(
"Trying out links [1234567890123456789…](https://map.vault48.org/test_abc_def_ghi) 123"
);
expect(
transformMDLinks(
"Trying out links [#alias|12345678901234567890123|map.vault48.org/test_abc_def_ghi] 123"
)
).toBe(
"Trying out links [1234567890123456789…](https://map.vault48.org/test_abc_def_ghi) 123"
);
});
});

View file

@ -498,15 +498,5 @@
"text": "Каракан 11 12 июня 2023: Итоги \n \n+ Покат был максимально лайтовым. Никакого лосизма, никаких продираний сквозь заросли, никаких тасканий велов, никаких преодолений. \n+ Путь от дома до пункта назначения занял всего 9 часов. Примерно в 15:10 были на берегу, а это значит, что времени на отдых, многократное употребление еды и исследование окрестностей осталось предостаточно. \n+ Пыльная дорога, отсыпанная щебнем между Быстровкой и Завьялово настолько короткая (около 7 км и преодолевается примерно за 20 минут) что неудобствами, которые она доставляет, можно пренебречь. \n+ Лесная дорога от Факела Революции до берега безупречна. Только ради неё стоит ехать в Каракан. Каждый велосипедист хотя бы раз в жизни должен по ней проехать. \n+ С клещами не сталкивались, комары встречались редко, были добрые и совсем не кусались. \n+ Обратно выехали не очень рано, часов в 10. В город приехали не очень поздно, часов в 19 и это, пожалуй, идеально. Достаточно времени на то чтобы выспаться и разобрать снарягу. \n \nНевыносимую печаль в этой поездке вызывали вереницы лесовозов и горы из стволов деревьев вдоль дороги. Не упускайте возможности скатать в Караканский бор, пока его не выпилили. \n \nФотографии с поката: https://vk.com/album-124752609_293941505\nПрямые трансляции НВС покатов: https://t.me/pogonia_live \nПодразделение НВС для тех, кто любит писать, что не приедет на покат: https://vk.com/nvs_sportloto",
"created": "2023-06-13 23:13:27",
"date": "2023-06-11 15:10:00"
},
{
"text": "Ленивый Ленинкат\n\n22.02.2025\n\nСтарт в 11:00 с площади Ленина.",
"created": "2025-02-20 23:13:27",
"date": "2025-02-22 11:00:00"
},
{
"text": "Ленивый Ленинкат\n\n22.02.25\n\nСтарт в 11:00 с площади Ленина.",
"created": "2025-02-20 23:13:27",
"date": "2025-02-22 11:00:00"
}
]

17
src/utils/extract.ts Normal file
View file

@ -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) as URL[];
};

View file

@ -1,57 +0,0 @@
import { URL } from "url";
const simpleUrlRegex = /(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;
/** Yep, that's how VK posts it's links */
const weirdLongUrlRegex = /\[\#alias\|([^\|]+)\|([^\]]+)\]/gim;
const fixUrl = (url: string) =>
url.startsWith("http") || !url ? url : `https://${url}`;
/** Extracts URLs from text */
export const extractURLs = (text: string): URL[] => {
const urls = new Set<string>();
text
.match(weirdLongUrlRegex)
?.forEach((match) =>
urls.add(fixUrl(match.replace(weirdLongUrlRegex, "$1")))
);
text.match(simpleUrlRegex)?.forEach((match) => urls.add(match));
return Array.from(urls)
.map((m) => {
try {
return new URL(m);
} catch (e) {
return;
}
})
.filter((el) => el) as URL[];
};
/** Adds ... to text if its length exceeds maxLength */
const trimTo = (val: string, maxLength: number) =>
val.length > maxLength ? val.substring(0, maxLength - 1).concat("…") : val;
/** Formatting all links in markdown output, trimming them to reasonable length */
export const transformMDLinks = (value: string) =>
value
.replace(weirdLongUrlRegex, (val, ...args) => {
if (args.length < 2) {
return val;
}
const title = trimTo(args[0] ?? args[1], 20);
const url = fixUrl(args[1]);
return `[${title}](${url})`;
})
.replace(simpleUrlRegex, (val) => {
if (val.endsWith(")")) {
return val;
}
return `[${trimTo(val, 20)}](${val})`;
});

View file

@ -0,0 +1,10 @@
const urlRegex = /(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,})/g;
const trimTo = (val: string, maxLength: number) =>
val.length > maxLength ? val.substring(0, maxLength - 1).concat("…") : val;
/** Formatting all links in markdown output, trimming them to reasonable length */
export default (value: string) =>
value.replace(urlRegex, (val) => {
return `[${trimTo(val, 20)}](${val})`;
});

View file

@ -4,7 +4,7 @@
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"outDir": "./output/",
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,

View file

@ -830,11 +830,6 @@
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e"
integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g==
"@vercel/ncc@^0.38.3":
version "0.38.3"
resolved "https://registry.yarnpkg.com/@vercel/ncc/-/ncc-0.38.3.tgz#5475eeee3ac0f1a439f237596911525a490a88b5"
integrity sha512-rnK6hJBS6mwc+Bkab+PGPs9OiS0i/3kdTO+CkI8V0/VrW3vmz7O2Pxjw/owOlmo6PKEIxRSeZKv/kuL9itnpYA==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@ -3822,7 +3817,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -3840,15 +3835,6 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@ -3877,7 +3863,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -3891,13 +3877,6 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -4402,16 +4381,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==