mirror of
https://github.com/thedevs-network/the-guard-bot
synced 2025-08-22 01:49:29 +00:00
Updates
This commit is contained in:
parent
0f1bcaae88
commit
37974a3d92
@ -1,26 +1,26 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
|
||||
working_directory: ~/repo
|
||||
working_directory: ~/repo
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
- v2-dependencies-
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
- v2-dependencies-
|
||||
|
||||
- run: npm ci
|
||||
- run: npm ci
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- "$HOME/.npm"
|
||||
key: v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
- save_cache:
|
||||
paths:
|
||||
- '$HOME/.npm'
|
||||
key: v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
|
||||
- run: npm run -s typecheck
|
||||
- run: npm run -s typecheck
|
||||
|
||||
- run: npm run -s lint
|
||||
- run: npm run -s lint
|
||||
|
@ -18,56 +18,35 @@
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/@typescript-eslint"
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-ignore": "warn",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/prefer-optional-chain": "error"
|
||||
"@typescript-eslint/prefer-optional-chain": "error",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "warn"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-mixed-spaces-and-tabs": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"semi": ["error", "always"],
|
||||
"@typescript-eslint/no-base-to-string": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"no-console": [ "error", { "allow": [ "assert" ] } ],
|
||||
"no-console": ["error", { "allow": ["assert"] }],
|
||||
"for-direction": "error",
|
||||
"no-await-in-loop": "error",
|
||||
"no-extra-parens": "error",
|
||||
"no-template-curly-in-string": "error",
|
||||
"accessor-pairs": "error",
|
||||
"array-callback-return": "error",
|
||||
"block-scoped-var": "error",
|
||||
"class-methods-use-this": "error",
|
||||
"consistent-return": "error",
|
||||
"curly": [
|
||||
"error",
|
||||
"multi-line"
|
||||
],
|
||||
"default-case": "error",
|
||||
"dot-location": [
|
||||
"error",
|
||||
"property"
|
||||
],
|
||||
"dot-location": ["error", "property"],
|
||||
"dot-notation": "error",
|
||||
"eqeqeq": "error",
|
||||
"no-alert": "error",
|
||||
@ -123,17 +102,12 @@
|
||||
],
|
||||
"require-await": "error",
|
||||
"vars-on-top": "error",
|
||||
"strict": [
|
||||
"error",
|
||||
"global"
|
||||
],
|
||||
"no-catch-shadow": "error",
|
||||
"strict": ["error", "global"],
|
||||
"no-label-var": "error",
|
||||
"no-shadow": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-undefined": "error",
|
||||
"no-use-before-define": "error",
|
||||
"@typescript-eslint/no-use-before-define": "error",
|
||||
"global-require": "error",
|
||||
"handle-callback-err": "error",
|
||||
"no-buffer-constructor": "error",
|
||||
@ -141,26 +115,16 @@
|
||||
"no-new-require": "error",
|
||||
"no-path-concat": "error",
|
||||
"no-process-exit": "error",
|
||||
"array-bracket-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"block-spacing": "error",
|
||||
"brace-style": "error",
|
||||
"comma-spacing": "error",
|
||||
"comma-style": "error",
|
||||
"computed-property-spacing": "error",
|
||||
"consistent-this": [
|
||||
"error",
|
||||
"self"
|
||||
],
|
||||
"consistent-this": ["error", "self"],
|
||||
"eol-last": "error",
|
||||
"func-call-spacing": "error",
|
||||
"func-name-matching": "error",
|
||||
"func-names": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"func-names": ["error", "as-needed"],
|
||||
"func-style": [
|
||||
"error",
|
||||
"declaration",
|
||||
@ -168,12 +132,9 @@
|
||||
"allowArrowFunctions": true
|
||||
}
|
||||
],
|
||||
"function-paren-newline": ["error", "multiline-arguments"],
|
||||
"comma-dangle": ["warn", "always-multiline"],
|
||||
"key-spacing": "error",
|
||||
"keyword-spacing": "error",
|
||||
"lines-around-comment": "error",
|
||||
"max-len": ["error", {"ignoreTemplateLiterals": true}],
|
||||
"max-len": ["error", { "ignoreTemplateLiterals": true }],
|
||||
"max-nested-callbacks": "error",
|
||||
"max-params": "error",
|
||||
"max-statements": "off",
|
||||
@ -195,10 +156,7 @@
|
||||
"no-underscore-dangle": "off",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"object-property-newline": [
|
||||
"error",
|
||||
{
|
||||
@ -206,7 +164,6 @@
|
||||
}
|
||||
],
|
||||
"operator-assignment": "error",
|
||||
"operator-linebreak": "error",
|
||||
"quote-props": [
|
||||
"error",
|
||||
"as-needed",
|
||||
@ -215,15 +172,6 @@
|
||||
"numbers": false
|
||||
}
|
||||
],
|
||||
"require-jsdoc": [
|
||||
"off",
|
||||
{
|
||||
"require": {
|
||||
"MethodDefinition": true,
|
||||
"ClassDeclaration": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"semi-spacing": "error",
|
||||
"semi-style": "error",
|
||||
"sort-vars": [
|
||||
|
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@ -1,6 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
"recommendations": ["editorconfig.editorconfig", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -3,7 +3,6 @@
|
||||
"editor.rulers": []
|
||||
},
|
||||
"eslint.validate": ["javascript", "typescript"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
|
91
README.md
91
README.md
@ -10,27 +10,30 @@ Initially created to moderate [The Devs Network](https://thedevs.network).
|
||||
**it has known issues, but it's successfully being used in production**
|
||||
|
||||
## Table of Contents
|
||||
* [Key Features](#key-features)
|
||||
* [Setup](#setup)
|
||||
* [Commands](#commands)
|
||||
* [Plugins](#plugins)
|
||||
* [Support](#support)
|
||||
* [License](#license)
|
||||
|
||||
- [Key Features](#key-features)
|
||||
- [Setup](#setup)
|
||||
- [Commands](#commands)
|
||||
- [Plugins](#plugins)
|
||||
- [Support](#support)
|
||||
- [License](#license)
|
||||
|
||||
## Key Features
|
||||
* Synchronized across multiple groups.
|
||||
* Adding admins to the bot.
|
||||
* Auto-remove and warn channels and groups ads.
|
||||
* Kick bots added by users.
|
||||
* Warn and ban users to control the group.
|
||||
* Commands work with replying, mentioning and ID.
|
||||
* Removes commands and temporary bot messages.
|
||||
* Ability to create custom commands.
|
||||
* Supports plugins.
|
||||
|
||||
- Synchronized across multiple groups.
|
||||
- Adding admins to the bot.
|
||||
- Auto-remove and warn channels and groups ads.
|
||||
- Kick bots added by users.
|
||||
- Warn and ban users to control the group.
|
||||
- Commands work with replying, mentioning and ID.
|
||||
- Removes commands and temporary bot messages.
|
||||
- Ability to create custom commands.
|
||||
- Supports plugins.
|
||||
|
||||
Overall, keeps the groups clean and healthy to use.
|
||||
|
||||
## Setup
|
||||
|
||||
You need [Node.js](https://nodejs.org/) (>= 12) to run this bot.
|
||||
|
||||
1. Create a bot via [@BotFather](https://t.me/BotFather) and grab a **token**.
|
||||
@ -40,6 +43,7 @@ You need [Node.js](https://nodejs.org/) (>= 12) to run this bot.
|
||||
5. Start the bot via `npm start`.
|
||||
|
||||
### Setup with Docker
|
||||
|
||||
You need to have [docker](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/#install-from-a-package) installed on your machine.
|
||||
|
||||
1. Create a bot via [@BotFather](https://t.me/BotFather) and grab a **token**.
|
||||
@ -51,29 +55,30 @@ You need to have [docker](https://docs.docker.com/engine/installation/linux/dock
|
||||
Now you can add the bot as **administrator** to your groups.
|
||||
|
||||
## Commands
|
||||
Command | Role | Available at | Description
|
||||
----------------------- | ---------- | ------------ | -----------------
|
||||
`/admin` | _Master_ | _Everywhere_ | Makes the user admin in the bot and groups.
|
||||
`/unadmin` | _Master_ | _Everywhere_ | Demotes the user from admin list.
|
||||
`/leave <name\|id>` | _Master_ | _Everywhere_ | Make the bot to leave the group cleanly.
|
||||
`/hidegroup` | _Master_ | _Groups_ | Revoke invite link and hide the group from `/groups` list.
|
||||
`/showgroup` | _Master_ | _Groups_ | Make the group accessible via `/groups` list.
|
||||
`/del [reason]` | _Admin_ | _Everywhere_ | Deletes replied-to message.
|
||||
`/warn <reason>` | _Admin_ | _Groups_ | Warns the user.
|
||||
`/unwarn` | _Admin_ | _Everywhere_ | Removes the last warn from the user.
|
||||
`/nowarns` | _Admin_ | _Everywhere_ | Clears warns for the user.
|
||||
`/permit` | _Admin_ | _Everywhere_ | Permits the user to advertise once, within 24 hours.
|
||||
`/ban <reason>` | _Admin_ | _Groups_ | Bans the user from groups.
|
||||
`/unban` | _Admin_ | _Everywhere_ | Removes the user from ban list.
|
||||
`/user` | _Admin_ | _Everywhere_ | Shows the status of the user.
|
||||
`/addcommand <name>` | _Admin_ | _In-Bot_ | Create a custom command.
|
||||
`/removecommand <name>` | _Admin_ | _In-Bot_ | Remove a custom command.
|
||||
`/staff` | _Everyone_ | _Everywhere_ | Shows a list of admins.
|
||||
`/link` | _Everyone_ | _Everywhere_ | Shows the current group's link.
|
||||
`/groups` | _Everyone_ | _Everywhere_ | Shows a list of groups which the bot is admin in.
|
||||
`/report` | _Everyone_ | _Everywhere_ | Reports the replied-to message to admins.
|
||||
`/commands` | _Everyone_ | _In-Bot_ | Shows a list of available commands.
|
||||
`/help` \| `/start` | _Everyone_ | _In-Bot_ | How to use the bot.
|
||||
|
||||
| Command | Role | Available at | Description |
|
||||
| ----------------------- | ---------- | ------------ | ---------------------------------------------------------- |
|
||||
| `/admin` | _Master_ | _Everywhere_ | Makes the user admin in the bot and groups. |
|
||||
| `/unadmin` | _Master_ | _Everywhere_ | Demotes the user from admin list. |
|
||||
| `/leave <name\|id>` | _Master_ | _Everywhere_ | Make the bot to leave the group cleanly. |
|
||||
| `/hidegroup` | _Master_ | _Groups_ | Revoke invite link and hide the group from `/groups` list. |
|
||||
| `/showgroup` | _Master_ | _Groups_ | Make the group accessible via `/groups` list. |
|
||||
| `/del [reason]` | _Admin_ | _Everywhere_ | Deletes replied-to message. |
|
||||
| `/warn <reason>` | _Admin_ | _Groups_ | Warns the user. |
|
||||
| `/unwarn` | _Admin_ | _Everywhere_ | Removes the last warn from the user. |
|
||||
| `/nowarns` | _Admin_ | _Everywhere_ | Clears warns for the user. |
|
||||
| `/permit` | _Admin_ | _Everywhere_ | Permits the user to advertise once, within 24 hours. |
|
||||
| `/ban <reason>` | _Admin_ | _Groups_ | Bans the user from groups. |
|
||||
| `/unban` | _Admin_ | _Everywhere_ | Removes the user from ban list. |
|
||||
| `/user` | _Admin_ | _Everywhere_ | Shows the status of the user. |
|
||||
| `/addcommand <name>` | _Admin_ | _In-Bot_ | Create a custom command. |
|
||||
| `/removecommand <name>` | _Admin_ | _In-Bot_ | Remove a custom command. |
|
||||
| `/staff` | _Everyone_ | _Everywhere_ | Shows a list of admins. |
|
||||
| `/link` | _Everyone_ | _Everywhere_ | Shows the current group's link. |
|
||||
| `/groups` | _Everyone_ | _Everywhere_ | Shows a list of groups which the bot is admin in. |
|
||||
| `/report` | _Everyone_ | _Everywhere_ | Reports the replied-to message to admins. |
|
||||
| `/commands` | _Everyone_ | _In-Bot_ | Shows a list of available commands. |
|
||||
| `/help` \| `/start` | _Everyone_ | _In-Bot_ | How to use the bot. |
|
||||
|
||||
All commands and actions are synchronized across all of the groups managed by the owner and they work with **replying**, **mentioning** or **ID** of a user.
|
||||
|
||||
@ -82,14 +87,14 @@ If used by reply, `/ban` and `/warn` would remove the replied-to message.
|
||||
## Plugins
|
||||
|
||||
The guard is extensible in form of plugins where custom features and commands can be easily added to it. To use a plugin, put it in the [/plugins](/plugins) directory and add its name to plugins array in config.js.
|
||||
Checkout our [Plugins' Wiki](https://github.com/thedevs-network/the-guard-bot/wiki/Plugins) page for more information.
|
||||
Checkout our [Plugins' Wiki](https://github.com/thedevs-network/the-guard-bot/wiki/Plugins) page for more information.
|
||||
|
||||
**Known plugins**:
|
||||
|
||||
* [Captcha](https://gist.github.com/poeti8/d84dfc4538510366a2d89294ff52b4ae): Adds a simple captcha to the bot to kick spam bots on join.
|
||||
* [Banfiles](https://gist.github.com/poeti8/133796200d66049c9bd58e6265a52f68): Ban users that send files with specified extentions.
|
||||
* [Anti Arabic](https://gist.github.com/poeti8/966ccef35d61ad2735dc0120ce3e8760): Bans users that send an Arabic/Persian message.
|
||||
* [Anti X-POST](https://gist.github.com/poeti8/c3057f973466676ca8dbbb1183cd0624): Removes same messages sent by user across one or multiple groups.
|
||||
- [Captcha](https://gist.github.com/poeti8/d84dfc4538510366a2d89294ff52b4ae): Adds a simple captcha to the bot to kick spam bots on join.
|
||||
- [Banfiles](https://gist.github.com/poeti8/133796200d66049c9bd58e6265a52f68): Ban users that send files with specified extentions.
|
||||
- [Anti Arabic](https://gist.github.com/poeti8/966ccef35d61ad2735dc0120ce3e8760): Bans users that send an Arabic/Persian message.
|
||||
- [Anti X-POST](https://gist.github.com/poeti8/c3057f973466676ca8dbbb1183cd0624): Removes same messages sent by user across one or multiple groups.
|
||||
|
||||
## Support
|
||||
|
||||
|
@ -16,8 +16,9 @@ module.exports = async ({ admin, reason, userToBan }) => {
|
||||
|
||||
await ban(userToBan, { by_id, date, reason });
|
||||
|
||||
await pMap(await listVisibleGroups(), group =>
|
||||
telegram.kickChatMember(group.id, userToBan.id));
|
||||
await pMap(await listVisibleGroups(), (group) =>
|
||||
telegram.banChatMember(group.id, userToBan.id)
|
||||
);
|
||||
|
||||
return html`
|
||||
🚫 ${lrm}${admin.first_name} <b>banned</b> ${displayUser(userToBan)}.
|
||||
|
@ -14,7 +14,8 @@ const ms = require('millisecond');
|
||||
const z = (n, d = 2) => String(n).padStart(d, '0');
|
||||
|
||||
/** @type {(d: Date) => string} */
|
||||
const yyyymmdd = d => `${z(d.getFullYear(), 4)}-${z(d.getMonth() + 1)}-${z(d.getDate())}`;
|
||||
const yyyymmdd = (d) =>
|
||||
`${z(d.getFullYear(), 4)}-${z(d.getMonth() + 1)}-${z(d.getDate())}`;
|
||||
|
||||
/** @type {(a: number, b: number) => number} */
|
||||
const cmp = (a, b) => Math.sign(a - b);
|
||||
@ -26,7 +27,7 @@ module.exports = async ({ admin, amend, reason, userToWarn }) => {
|
||||
const { warns } = await warn(
|
||||
userToWarn,
|
||||
{ by_id, date, reason },
|
||||
{ amend },
|
||||
{ amend }
|
||||
);
|
||||
|
||||
const recentWarns = warns.filter(isWarnNotExpired(date));
|
||||
@ -35,13 +36,21 @@ module.exports = async ({ admin, amend, reason, userToWarn }) => {
|
||||
'-1': html`<b>${recentWarns.length}</b>/${numberOfWarnsToBan}`,
|
||||
0: html`<b>Final warning</b>`,
|
||||
// eslint-disable-next-line max-len
|
||||
1: html`<b>${recentWarns.length}</b>/${numberOfWarnsToBan} (🚫 <b>banned</b>)`,
|
||||
1: html`<b>${recentWarns.length}</b>/${numberOfWarnsToBan} (🚫
|
||||
<b>banned</b>)`,
|
||||
}[cmp(recentWarns.length + 1, numberOfWarnsToBan)];
|
||||
|
||||
const expiryText =
|
||||
typeof expireWarnsAfter === 'undefined' || expireWarnsAfter === Infinity
|
||||
? ''
|
||||
: `Expires on ${yyyymmdd(
|
||||
new Date(date.getTime() + ms(expireWarnsAfter))
|
||||
)}`;
|
||||
|
||||
const warnMessage = html`
|
||||
⚠️ ${lrm}${admin.first_name} <b>warned</b> ${link(userToWarn)}.
|
||||
${count}: ${lrm}${reason}
|
||||
<i>${typeof expireWarnsAfter === 'undefined' || expireWarnsAfter === Infinity ? '' : `Expires on ${yyyymmdd(new Date(date.getTime() + ms(expireWarnsAfter)))}`}</i>
|
||||
<i>${expiryText}</i>
|
||||
`;
|
||||
|
||||
if (recentWarns.length >= numberOfWarnsToBan) {
|
||||
|
@ -12,9 +12,10 @@ const {
|
||||
deleteBansAfter = false,
|
||||
} = require('../utils/config').config;
|
||||
|
||||
const normalisedDeleteWarnsAfter = typeof deleteWarnsAfter === 'object'
|
||||
? { auto: false, manual: false, ...deleteWarnsAfter }
|
||||
: { auto: deleteWarnsAfter, manual: deleteWarnsAfter };
|
||||
const normalisedDeleteWarnsAfter =
|
||||
typeof deleteWarnsAfter === 'object'
|
||||
? { auto: false, manual: false, ...deleteWarnsAfter }
|
||||
: { auto: deleteWarnsAfter, manual: deleteWarnsAfter };
|
||||
|
||||
const reply_markup = { inline_keyboard: warnInlineKeyboard };
|
||||
|
||||
@ -22,27 +23,30 @@ const reply_markup = { inline_keyboard: warnInlineKeyboard };
|
||||
module.exports = {
|
||||
async ban({ admin, reason, userToBan }) {
|
||||
const banMessage = await ban({ admin, reason, userToBan });
|
||||
return this.loggedReply(banMessage)
|
||||
.then(scheduleDeletion(deleteBansAfter));
|
||||
return this.loggedReply(banMessage).then(
|
||||
scheduleDeletion(deleteBansAfter)
|
||||
);
|
||||
},
|
||||
async batchBan({ admin, reason, targets }) {
|
||||
const banMessage = await batchBan({ admin, reason, targets });
|
||||
return this.loggedReply(banMessage)
|
||||
.then(scheduleDeletion(deleteBansAfter));
|
||||
return this.loggedReply(banMessage).then(
|
||||
scheduleDeletion(deleteBansAfter)
|
||||
);
|
||||
},
|
||||
async warn({ admin, amend, reason, userToWarn, mode }) {
|
||||
const warnMessage = await warn({ admin, amend, reason, userToWarn });
|
||||
return this.loggedReply(warnMessage, { reply_markup })
|
||||
.then(scheduleDeletion(normalisedDeleteWarnsAfter[mode]));
|
||||
return this.loggedReply(warnMessage, { reply_markup }).then(
|
||||
scheduleDeletion(normalisedDeleteWarnsAfter[mode])
|
||||
);
|
||||
},
|
||||
|
||||
loggedReply(html, extra) {
|
||||
if (chats.adminLog) {
|
||||
this.tg
|
||||
this.telegram
|
||||
.sendMessage(
|
||||
chats.adminLog,
|
||||
html.toJSON().replace(/\[<code>(\d+)<\/code>\]/g, '[#u$1]'),
|
||||
{ parse_mode: 'HTML' },
|
||||
{ parse_mode: 'HTML' }
|
||||
)
|
||||
.catch(() => null);
|
||||
}
|
||||
@ -50,6 +54,54 @@ module.exports = {
|
||||
},
|
||||
|
||||
replyWithCopy(content, options) {
|
||||
return this.telegram.sendCopy(this.chat.id, content, options);
|
||||
if ('text' in content) {
|
||||
return this.telegram.sendMessage(this.chat.id, content.text, {
|
||||
...options,
|
||||
entities: content.entities,
|
||||
});
|
||||
}
|
||||
if ('photo' in content) {
|
||||
return this.telegram.sendPhoto(
|
||||
this.chat.id,
|
||||
content.photo.at(-1).file_id,
|
||||
{
|
||||
...options,
|
||||
caption: content.caption,
|
||||
caption_entities: content.caption_entities,
|
||||
}
|
||||
);
|
||||
}
|
||||
if ('video' in content) {
|
||||
return this.telegram.sendVideo(
|
||||
this.chat.id,
|
||||
content.video.at(-1).file_id,
|
||||
{
|
||||
...options,
|
||||
caption: content.caption,
|
||||
caption_entities: content.caption_entities,
|
||||
}
|
||||
);
|
||||
}
|
||||
if ('video_note' in content) {
|
||||
return this.telegram.sendVideoNote(
|
||||
this.chat.id,
|
||||
content.video_note.file_id,
|
||||
{
|
||||
...options,
|
||||
thumb: content.video_note.thumb?.file_id,
|
||||
length: content.video_note.length,
|
||||
duration: content.video_note.duration,
|
||||
}
|
||||
);
|
||||
}
|
||||
return this.telegram.sendMessage(
|
||||
this.chat.id,
|
||||
`❌ <i>Unsupported message</i> <pre><code class="language-json">${JSON.stringify(
|
||||
content,
|
||||
null,
|
||||
2
|
||||
)}</code></pre>`,
|
||||
{ ...options, parse_mode: 'HTML' }
|
||||
);
|
||||
},
|
||||
};
|
||||
|
11
bot/index.js
11
bot/index.js
@ -18,10 +18,13 @@ if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
module.exports = bot;
|
||||
|
||||
// Otherwise the bot can't ban on reaching max warns due to botInfo not being available to get its admin ID
|
||||
Object.defineProperty(bot.context, "botInfo", {
|
||||
get () { return bot.botInfo; }
|
||||
})
|
||||
// Otherwise the bot can't ban on reaching max warns due to botInfo not being
|
||||
// available to get its admin ID
|
||||
Object.defineProperty(bot.context, 'botInfo', {
|
||||
get() {
|
||||
return bot.botInfo;
|
||||
},
|
||||
});
|
||||
|
||||
// cyclic dependency
|
||||
// bot/index requires context requires actions/warn requires bot/index
|
||||
|
@ -25,7 +25,6 @@
|
||||
* @type {Config}
|
||||
*/
|
||||
const config = {
|
||||
|
||||
/**
|
||||
* @type {!( number | string | (number|string)[] )}
|
||||
* ID (number) or username (string) of master,
|
||||
@ -40,9 +39,7 @@ const config = {
|
||||
*/
|
||||
token: '',
|
||||
|
||||
|
||||
chats: {
|
||||
|
||||
/**
|
||||
* @type {(number | false)}
|
||||
* Chat to send member join/leave notifications to.
|
||||
@ -67,7 +64,7 @@ const config = {
|
||||
|
||||
deleteCustom: {
|
||||
longerThan: 450, // UTF-16 characters
|
||||
after: '20 minutes'
|
||||
after: '20 minutes',
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -35,9 +35,7 @@ const roleKbRow = (cmdData) => [
|
||||
|
||||
const normalizeRole = (role = '') => {
|
||||
const lower = role.toLowerCase();
|
||||
return lower === 'master' || lower === 'admins'
|
||||
? lower
|
||||
: 'everyone';
|
||||
return lower === 'master' || lower === 'admins' ? lower : 'everyone';
|
||||
};
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
@ -48,7 +46,7 @@ const addCommandHandler = async (ctx) => {
|
||||
|
||||
if (ctx.from.status !== 'admin') {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Sorry, only admins access this command.</b>',
|
||||
'ℹ️ <b>Sorry, only admins access this command.</b>'
|
||||
);
|
||||
}
|
||||
|
||||
@ -59,13 +57,15 @@ const addCommandHandler = async (ctx) => {
|
||||
if (!isValidName) {
|
||||
return ctx.replyWithHTML(
|
||||
'<b>Send a valid command.</b>\n\nExample:\n' +
|
||||
'<code>/addcommand rules</code>',
|
||||
'<code>/addcommand rules</code>'
|
||||
);
|
||||
}
|
||||
const newCommand = isValidName[1].toLowerCase();
|
||||
if (preserved.has(newCommand)) {
|
||||
return reply('❗️ Sorry you can\'t use this name, it\'s preserved.\n\n' +
|
||||
'Try another one.');
|
||||
return reply(
|
||||
"❗️ Sorry you can't use this name, it's preserved.\n\n" +
|
||||
'Try another one.'
|
||||
);
|
||||
}
|
||||
|
||||
const replaceCmd = flags.has('replace');
|
||||
@ -76,19 +76,19 @@ const addCommandHandler = async (ctx) => {
|
||||
if (!replaceCmd && cmdExists) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>This command already exists.</b>\n\n' +
|
||||
'/commands - to see the list of commands.\n' +
|
||||
'/addcommand <code><name></code> - to add a command.\n' +
|
||||
'/removecommand <code><name></code>' +
|
||||
' - to remove a command.',
|
||||
Markup.keyboard([ [ `/addcommand -replace ${newCommand}` ] ])
|
||||
'/commands - to see the list of commands.\n' +
|
||||
'/addcommand <code><name></code> - to add a command.\n' +
|
||||
'/removecommand <code><name></code>' +
|
||||
' - to remove a command.',
|
||||
Markup.keyboard([[`/addcommand -replace ${newCommand}`]])
|
||||
.selective()
|
||||
.oneTime()
|
||||
.resize(),
|
||||
.resize()
|
||||
);
|
||||
}
|
||||
if (cmdExists && cmdExists.role === 'master' && !isMaster(ctx.from)) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Sorry, only master can replace this command.</b>',
|
||||
'ℹ️ <b>Sorry, only master can replace this command.</b>'
|
||||
);
|
||||
}
|
||||
|
||||
@ -102,17 +102,17 @@ const addCommandHandler = async (ctx) => {
|
||||
caption: null,
|
||||
isActive: true,
|
||||
name: newCommand,
|
||||
...softReplace || { content },
|
||||
...(softReplace || { content }),
|
||||
});
|
||||
return ctx.replyWithHTML(
|
||||
`✅ <b>Successfully added <code>!${isValidName[1]}</code></b>.\n` +
|
||||
'Who should be able to use it?',
|
||||
inlineKeyboard(roleKbRow({ currentRole: role, newCommand })),
|
||||
'Who should be able to use it?',
|
||||
inlineKeyboard(roleKbRow({ currentRole: role, newCommand }))
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
return ctx.replyWithHTML('ℹ️ <b>Reply to a message you\'d like to save</b>');
|
||||
return ctx.replyWithHTML("ℹ️ <b>Reply to a message you'd like to save</b>");
|
||||
};
|
||||
|
||||
module.exports = addCommandHandler;
|
||||
|
@ -7,10 +7,7 @@ const { link, scheduleDeletion } = require('../../utils/tg');
|
||||
const { parse, strip } = require('../../utils/cmd');
|
||||
|
||||
// DB
|
||||
const {
|
||||
admin,
|
||||
getUser,
|
||||
} = require('../../stores/user');
|
||||
const { admin, getUser } = require('../../stores/user');
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const adminHandler = async (ctx) => {
|
||||
@ -19,9 +16,9 @@ const adminHandler = async (ctx) => {
|
||||
const { targets } = parse(ctx.message);
|
||||
|
||||
if (targets.length > 1) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify one user to promote.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify one user to promote.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const userToAdmin = targets.length
|
||||
@ -29,25 +26,29 @@ const adminHandler = async (ctx) => {
|
||||
: ctx.from;
|
||||
|
||||
if (!userToAdmin) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>\n' +
|
||||
'Please forward their message, then try again.',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>\n' +
|
||||
'Please forward their message, then try again.'
|
||||
)
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (userToAdmin.status === 'banned') {
|
||||
return ctx.replyWithHTML('ℹ️ <b>Can\'t admin banned user.</b>');
|
||||
return ctx.replyWithHTML("ℹ️ <b>Can't admin banned user.</b>");
|
||||
}
|
||||
|
||||
if (userToAdmin.status === 'admin') {
|
||||
return ctx.replyWithHTML(
|
||||
html`⭐️ ${link(userToAdmin)} <b>is already admin.</b>`,
|
||||
html`⭐️ ${link(userToAdmin)} <b>is already admin.</b>`
|
||||
);
|
||||
}
|
||||
|
||||
await admin(userToAdmin);
|
||||
|
||||
return ctx.replyWithHTML(html`⭐️ ${link(userToAdmin)} <b>is now admin.</b>`);
|
||||
return ctx.replyWithHTML(
|
||||
html`⭐️ ${link(userToAdmin)} <b>is now admin.</b>`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = adminHandler;
|
||||
|
@ -14,7 +14,7 @@ const { getUser } = require('../../stores/user');
|
||||
const banHandler = async (ctx) => {
|
||||
if (ctx.chat.type === 'private') {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>This command is only available in groups.</b>',
|
||||
'ℹ️ <b>This command is only available in groups.</b>'
|
||||
);
|
||||
}
|
||||
|
||||
@ -23,13 +23,14 @@ const banHandler = async (ctx) => {
|
||||
const { flags, targets, reason } = parse(ctx.message);
|
||||
|
||||
if (targets.length === 0) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify at least one user to ban.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify at least one user to ban.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (reason.length === 0) {
|
||||
return ctx.replyWithHTML('ℹ️ <b>Need a reason to ban.</b>')
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Need a reason to ban.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
@ -37,35 +38,38 @@ const banHandler = async (ctx) => {
|
||||
return ctx.batchBan({ admin: ctx.from, reason, targets });
|
||||
}
|
||||
|
||||
const userToBan = await getUser(strip(targets[0])) || targets[0];
|
||||
const userToBan = (await getUser(strip(targets[0]))) || targets[0];
|
||||
|
||||
if (!userToBan.id) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>\n' +
|
||||
'Please forward their message, then try again.',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>\n' +
|
||||
'Please forward their message, then try again.'
|
||||
)
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (userToBan.id === ctx.botInfo.id) return null;
|
||||
|
||||
if (userToBan.status === 'admin') {
|
||||
return ctx.replyWithHTML('ℹ️ <b>Can\'t ban other admins.</b>');
|
||||
return ctx.replyWithHTML("ℹ️ <b>Can't ban other admins.</b>");
|
||||
}
|
||||
|
||||
if (ctx.message.reply_to_message) {
|
||||
ctx.deleteMessage(ctx.message.reply_to_message.message_id)
|
||||
.catch(() => null);
|
||||
ctx.deleteMessage(ctx.message.reply_to_message.message_id).catch(
|
||||
() => null
|
||||
);
|
||||
}
|
||||
|
||||
if (!flags.has('amend') && userToBan.status === 'banned') {
|
||||
return ctx.replyWithHTML(
|
||||
html`🚫 ${displayUser(userToBan)} <b>is already banned.</b>`,
|
||||
html`🚫 ${displayUser(userToBan)} <b>is already banned.</b>`
|
||||
);
|
||||
}
|
||||
|
||||
return ctx.ban({
|
||||
admin: ctx.from,
|
||||
reason: '[' + ctx.chat.title + '] ' + await substom(reason),
|
||||
reason: '[' + ctx.chat.title + '] ' + (await substom(reason)),
|
||||
userToBan,
|
||||
});
|
||||
};
|
||||
|
@ -49,29 +49,24 @@ const commandReferenceHandler = async (ctx) => {
|
||||
const customCommandsGrouped = R.groupBy(role, customCommands);
|
||||
const userCustomCommands = customCommandsGrouped.everyone
|
||||
? '[everyone]\n<code>' +
|
||||
customCommandsGrouped.everyone
|
||||
.map(name)
|
||||
.join(', ') +
|
||||
'</code>\n\n'
|
||||
customCommandsGrouped.everyone.map(name).join(', ') +
|
||||
'</code>\n\n'
|
||||
: '';
|
||||
|
||||
const adminCustomCommands = customCommandsGrouped.admins
|
||||
? '[admins]\n<code>' +
|
||||
customCommandsGrouped.admins
|
||||
.map(name)
|
||||
.join(', ') +
|
||||
'</code>\n\n'
|
||||
customCommandsGrouped.admins.map(name).join(', ') +
|
||||
'</code>\n\n'
|
||||
: '';
|
||||
|
||||
const masterCustomCommands = customCommandsGrouped.master
|
||||
? '[master]\n<code>' +
|
||||
customCommandsGrouped.master
|
||||
.map(name)
|
||||
.join(', ') +
|
||||
'</code>\n\n'
|
||||
customCommandsGrouped.master.map(name).join(', ') +
|
||||
'</code>\n\n'
|
||||
: '';
|
||||
|
||||
const customCommandsText = masterCommands.repeat(isMaster(ctx.from)) +
|
||||
const customCommandsText =
|
||||
masterCommands.repeat(isMaster(ctx.from)) +
|
||||
adminCommands.repeat(ctx.from && ctx.from.status === 'admin') +
|
||||
userCommands +
|
||||
'\n<b>Custom commands(prefix with !):</b>\n' +
|
||||
@ -79,8 +74,7 @@ const commandReferenceHandler = async (ctx) => {
|
||||
adminCustomCommands.repeat(ctx.from && ctx.from.status === 'admin') +
|
||||
userCustomCommands;
|
||||
|
||||
return ctx.replyWithHTML(customCommandsText)
|
||||
.then(scheduleDeletion());
|
||||
return ctx.replyWithHTML(customCommandsText).then(scheduleDeletion());
|
||||
};
|
||||
|
||||
module.exports = commandReferenceHandler;
|
||||
|
@ -17,19 +17,22 @@ module.exports = async (ctx) => {
|
||||
|
||||
if (!(flags.has('msg_id') || ctx.message.reply_to_message)) {
|
||||
// eslint-disable-next-line max-len
|
||||
await ctx.replyWithHTML('ℹ️ <b>Reply to a message you\'d like to delete</b>').then(scheduleDeletion());
|
||||
await ctx
|
||||
.replyWithHTML("ℹ️ <b>Reply to a message you'd like to delete</b>")
|
||||
.then(scheduleDeletion());
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.telegram.deleteMessage(
|
||||
flags.get('chat_id') || ctx.chat.id,
|
||||
flags.get('msg_id') || ctx.message.reply_to_message.message_id,
|
||||
flags.get('msg_id') || ctx.message.reply_to_message.message_id
|
||||
);
|
||||
|
||||
if (reason) {
|
||||
const id = R.path([ 'message', 'reply_to_message', 'from', 'id' ], ctx);
|
||||
const id = R.path(['message', 'reply_to_message', 'from', 'id'], ctx);
|
||||
const emoji = id ? link({ id, first_name: '🗑' }) : '🗑';
|
||||
await ctx.replyWithHTML(html`${emoji} ${reason}`)
|
||||
await ctx
|
||||
.replyWithHTML(html`${emoji} ${reason}`)
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
};
|
||||
|
@ -16,9 +16,10 @@ const inline_keyboard = config.groupsInlineKeyboard;
|
||||
|
||||
const reply_markup = inline_keyboard && { inline_keyboard };
|
||||
|
||||
const entry = group => group.username
|
||||
? `- @${group.username}`
|
||||
: TgHtml.tag`- <a href="${group.link}">${group.title}</a>`;
|
||||
const entry = (group) =>
|
||||
group.username
|
||||
? `- @${group.username}`
|
||||
: TgHtml.tag`- <a href="${group.link}">${group.title}</a>`;
|
||||
|
||||
const emojiRegex = XRegExp.tag('gx')`
|
||||
[\uE000-\uF8FF]|
|
||||
@ -27,21 +28,24 @@ const emojiRegex = XRegExp.tag('gx')`
|
||||
[\u2011-\u26FF]|
|
||||
\uD83E[\uDD10-\uDDFF]`;
|
||||
|
||||
const stripEmoji = s => s.replace(emojiRegex, '');
|
||||
const stripEmoji = (s) => s.replace(emojiRegex, '');
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const groupsHandler = async (ctx) => {
|
||||
const groups = await listVisibleGroups();
|
||||
|
||||
groups.sort((a, b) =>
|
||||
stripEmoji(a.title).localeCompare(stripEmoji(b.title)));
|
||||
stripEmoji(a.title).localeCompare(stripEmoji(b.title))
|
||||
);
|
||||
|
||||
const entries = TgHtml.join('\n', groups.map(entry));
|
||||
|
||||
return ctx.replyWithHTML(TgHtml.tag`🛠 <b>Groups I manage</b>:\n\n${entries}`, {
|
||||
disable_web_page_preview: true,
|
||||
reply_markup,
|
||||
}).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(TgHtml.tag`🛠 <b>Groups I manage</b>:\n\n${entries}`, {
|
||||
disable_web_page_preview: true,
|
||||
reply_markup,
|
||||
})
|
||||
.then(scheduleDeletion());
|
||||
};
|
||||
|
||||
module.exports = groupsHandler;
|
||||
|
@ -25,7 +25,7 @@ const helpHandler = (ctx) => {
|
||||
return ctx.replyWithHTML(
|
||||
message,
|
||||
Markup.inlineKeyboard([
|
||||
Markup.button.url('🛠 Setup a New Bot', homepage)
|
||||
Markup.button.url('🛠 Setup a New Bot', homepage),
|
||||
])
|
||||
);
|
||||
};
|
||||
|
@ -13,6 +13,6 @@ module.exports = router;
|
||||
const exclude = (_, filename) => filename === 'routingFn.js';
|
||||
const rename = R.toLower;
|
||||
|
||||
const extensions = [ 'js', 'ts' ];
|
||||
const extensions = ['js', 'ts'];
|
||||
const handlers = requireDir(module, { exclude, extensions, rename });
|
||||
router.handlers = new Map(Object.entries(handlers));
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { managesGroup, removeGroup } from "../../stores/group";
|
||||
import { ExtendedContext } from "../../typings/context";
|
||||
import { html } from "../../utils/html";
|
||||
import { isMaster } from "../../utils/config";
|
||||
import { managesGroup, removeGroup } from '../../stores/group';
|
||||
import { ExtendedContext } from '../../typings/context';
|
||||
import { html } from '../../utils/html';
|
||||
import { isMaster } from '../../utils/config';
|
||||
|
||||
const leaveCommandHandler = async (ctx: ExtendedContext) => {
|
||||
if (!isMaster(ctx.from)) return null;
|
||||
if (typeof ctx.message === 'undefined') return null;
|
||||
if (!('text' in ctx.message)) return null;
|
||||
|
||||
const query = ctx.message!.text!.split(" ").slice(1).join(" ");
|
||||
const query = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
|
||||
const group = query
|
||||
? await managesGroup(
|
||||
@ -14,7 +16,7 @@ const leaveCommandHandler = async (ctx: ExtendedContext) => {
|
||||
)
|
||||
: ctx.chat;
|
||||
if (!group) {
|
||||
return ctx.replyWithHTML("❓ <b>Unknown group.</b>");
|
||||
return ctx.replyWithHTML('❓ <b>Unknown group.</b>');
|
||||
}
|
||||
|
||||
await removeGroup(group);
|
||||
|
@ -14,9 +14,11 @@ const linkHandler = async (ctx, next) => {
|
||||
|
||||
const group = await managesGroup({ id: ctx.chat.id });
|
||||
|
||||
return ctx.replyWithHTML(group.link || '️ℹ️ <b>No link to this group</b>', {
|
||||
disable_web_page_preview: false,
|
||||
}).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(group.link || '️ℹ️ <b>No link to this group</b>', {
|
||||
disable_web_page_preview: false,
|
||||
})
|
||||
.then(scheduleDeletion());
|
||||
};
|
||||
|
||||
module.exports = linkHandler;
|
||||
|
@ -17,43 +17,48 @@ const { listGroups } = require('../../stores/group');
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const nowarnsHandler = async (ctx) => {
|
||||
if (ctx.from?.status !== 'admin') return null;
|
||||
if (typeof ctx.message === 'undefined') return null;
|
||||
if (!('text' in ctx.message)) return null;
|
||||
|
||||
const { targets } = parse(ctx.message);
|
||||
|
||||
if (targets.length !== 1) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify one user to pardon.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify one user to pardon.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const userToUnwarn = await getUser(strip(targets[0]));
|
||||
|
||||
if (!userToUnwarn) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('❓ <b>User unknown.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const { warns } = userToUnwarn;
|
||||
|
||||
if (warns.length === 0) {
|
||||
return ctx.replyWithHTML(
|
||||
html`ℹ️ ${link(userToUnwarn)} <b>already has no warnings.</b>`,
|
||||
html`ℹ️ ${link(userToUnwarn)} <b>already has no warnings.</b>`
|
||||
);
|
||||
}
|
||||
|
||||
if (userToUnwarn.status === 'banned') {
|
||||
await pMap(await listGroups({ type: 'supergroup' }), (group) =>
|
||||
ctx.telegram.unbanChatMember(group.id, userToUnwarn.id));
|
||||
ctx.telegram.unbanChatMember(group.id, userToUnwarn.id)
|
||||
);
|
||||
}
|
||||
|
||||
await nowarns(userToUnwarn);
|
||||
|
||||
if (userToUnwarn.status === 'banned') {
|
||||
ctx.telegram.sendMessage(
|
||||
userToUnwarn.id,
|
||||
'♻️ You were unbanned from all of the /groups!',
|
||||
).catch(() => null);
|
||||
ctx.telegram
|
||||
.sendMessage(
|
||||
userToUnwarn.id,
|
||||
'♻️ You were unbanned from all of the /groups!'
|
||||
)
|
||||
.catch(() => null);
|
||||
// it's likely that the banned person haven't PMed the bot,
|
||||
// which will cause the sendMessage to fail,
|
||||
// hance .catch(noop)
|
||||
@ -66,5 +71,4 @@ const nowarnsHandler = async (ctx) => {
|
||||
`);
|
||||
};
|
||||
|
||||
|
||||
module.exports = nowarnsHandler;
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { displayUser, scheduleDeletion } from "../../utils/tg";
|
||||
import { html, lrm } from "../../utils/html";
|
||||
import { parse, strip } from "../../utils/cmd";
|
||||
import type { ExtendedContext } from "../../typings/context";
|
||||
import { permit } from "../../stores/user";
|
||||
import { displayUser, scheduleDeletion } from '../../utils/tg';
|
||||
import { html, lrm } from '../../utils/html';
|
||||
import { parse, strip } from '../../utils/cmd';
|
||||
import type { ExtendedContext } from '../../typings/context';
|
||||
import { permit } from '../../stores/user';
|
||||
|
||||
export = async (ctx: ExtendedContext) => {
|
||||
if (ctx.from?.status !== "admin") return null;
|
||||
if (ctx.from?.status !== 'admin') return null;
|
||||
if (typeof ctx.message === 'undefined') return null;
|
||||
if (!('text' in ctx.message)) return null;
|
||||
|
||||
const { targets } = parse(ctx.message);
|
||||
if (targets.length !== 1) {
|
||||
return ctx
|
||||
.replyWithHTML("ℹ️ <b>Specify one user to permit.</b>")
|
||||
.replyWithHTML('ℹ️ <b>Specify one user to permit.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
@ -20,7 +22,9 @@ export = async (ctx: ExtendedContext) => {
|
||||
});
|
||||
|
||||
return ctx.replyWithHTML(html`
|
||||
🎟 ${lrm}${ctx.from.first_name} <b>permitted</b> ${displayUser(permitted)} to
|
||||
promote once within the next 24 hours!
|
||||
🎟 ${lrm}${ctx.from.first_name} <b>permitted</b> ${displayUser(
|
||||
permitted
|
||||
)}
|
||||
to promote once within the next 24 hours!
|
||||
`);
|
||||
};
|
||||
|
@ -12,35 +12,33 @@ const removeCommandHandler = async (ctx) => {
|
||||
|
||||
if (ctx.from.status !== 'admin') {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Sorry, only admins access this command.</b>',
|
||||
'ℹ️ <b>Sorry, only admins access this command.</b>'
|
||||
);
|
||||
}
|
||||
const [ , commandName ] = text.split(' ');
|
||||
const [, commandName] = text.split(' ');
|
||||
if (!commandName) {
|
||||
return ctx.replyWithHTML(
|
||||
'<b>Send a valid command.</b>\n\nExample:\n' +
|
||||
'<code>/removecommand rules</code>',
|
||||
'<code>/removecommand rules</code>'
|
||||
);
|
||||
}
|
||||
|
||||
const command = await getCommand({ name: commandName.toLowerCase() });
|
||||
if (!command) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Command couldn\'t be found.</b>',
|
||||
);
|
||||
return ctx.replyWithHTML("ℹ️ <b>Command couldn't be found.</b>");
|
||||
}
|
||||
|
||||
const role = command.role.toLowerCase();
|
||||
if (role === 'master' && !isMaster(ctx.from)) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Sorry, only master can remove this command.</b>',
|
||||
'ℹ️ <b>Sorry, only master can remove this command.</b>'
|
||||
);
|
||||
}
|
||||
|
||||
await removeCommand({ name: commandName.toLowerCase() });
|
||||
return ctx.replyWithHTML(
|
||||
`✅ <code>!${commandName}</code> ` +
|
||||
'<b>has been removed successfully.</b>',
|
||||
'<b>has been removed successfully.</b>'
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,38 +3,40 @@
|
||||
// Utils
|
||||
const Cmd = require('../../utils/cmd');
|
||||
const { TgHtml } = require('../../utils/html');
|
||||
const {
|
||||
link,
|
||||
msgLink,
|
||||
scheduleDeletion,
|
||||
} = require('../../utils/tg');
|
||||
const { link, msgLink, scheduleDeletion } = require('../../utils/tg');
|
||||
|
||||
const { chats = {} } = require('../../utils/config').config;
|
||||
|
||||
const isQualified = member => member.status === 'creator' ||
|
||||
member.can_delete_messages &&
|
||||
member.can_restrict_members;
|
||||
const isQualified = (member) =>
|
||||
member.status === 'creator' ||
|
||||
(member.can_delete_messages && member.can_restrict_members);
|
||||
|
||||
const adminMention = ({ user }) =>
|
||||
TgHtml.tag`<a href="tg://user?id=${user.id}">​</a>`;
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const reportHandler = async ctx => {
|
||||
const reportHandler = async (ctx) => {
|
||||
if (!ctx.chat.type.endsWith('group')) return null;
|
||||
// Ignore monospaced reports
|
||||
if (ctx.message.entities?.[0]?.type === 'code' && ctx.message.entities[0].offset === 0)
|
||||
if (
|
||||
ctx.message.entities?.[0]?.type === 'code' &&
|
||||
ctx.message.entities[0].offset === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!ctx.message.reply_to_message) {
|
||||
await ctx.deleteMessage();
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Reply to message you\'d like to report</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML("ℹ️ <b>Reply to message you'd like to report</b>")
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
const admins = (await ctx.getChatAdministrators())
|
||||
.filter(isQualified)
|
||||
.map(adminMention);
|
||||
// eslint-disable-next-line max-len
|
||||
const s = TgHtml.tag`❗️ <b>Message from ${link(ctx.message.reply_to_message.from)} was reported to the admins</b>.${TgHtml.join('', admins)}`;
|
||||
const s = TgHtml.tag`❗️ <b>Message from ${link(
|
||||
ctx.message.reply_to_message.from
|
||||
)} was reported to the admins</b>.${TgHtml.join('', admins)}`;
|
||||
const report = await ctx.replyWithHTML(s, {
|
||||
reply_to_message_id: ctx.message.reply_to_message.message_id,
|
||||
});
|
||||
@ -43,21 +45,30 @@ const reportHandler = async ctx => {
|
||||
await ctx.telegram.sendMessage(
|
||||
chats.report,
|
||||
TgHtml.tag`❗️ ${link(ctx.from)} reported <a href="${msgLink(
|
||||
ctx.message.reply_to_message,
|
||||
)}">a message</a> from ${link(ctx.message.reply_to_message.from)} in ${ctx.chat.title}!`,
|
||||
ctx.message.reply_to_message
|
||||
)}">a message</a> from ${link(
|
||||
ctx.message.reply_to_message.from
|
||||
)} in ${ctx.chat.title}!`,
|
||||
{
|
||||
parse_mode: 'HTML',
|
||||
reply_markup: { inline_keyboard: [ [ {
|
||||
text: '✔️ Handled',
|
||||
callback_data: Cmd.stringify({
|
||||
command: 'del',
|
||||
flags: {
|
||||
chat_id: report.chat.id,
|
||||
msg_id: report.message_id,
|
||||
},
|
||||
reason: 'Report handled',
|
||||
}),
|
||||
} ] ] } },
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{
|
||||
text: '✔️ Handled',
|
||||
callback_data: Cmd.stringify({
|
||||
command: 'del',
|
||||
flags: {
|
||||
chat_id: report.chat.id,
|
||||
msg_id: report.message_id,
|
||||
},
|
||||
reason: 'Report handled',
|
||||
}),
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -7,8 +7,9 @@ const { isCommand } = require('../../utils/tg');
|
||||
module.exports = ({ me, message }) => {
|
||||
if (!isCommand(message)) return null;
|
||||
|
||||
const [ , command, username ] =
|
||||
/^\/(?:start )?(\w+)(@\w+)?/.exec(message.text);
|
||||
const [, command, username] = /^\/(?:start )?(\w+)(@\w+)?/.exec(
|
||||
message.text
|
||||
);
|
||||
|
||||
if (username && !eq.username(username, me)) return null;
|
||||
|
||||
|
@ -8,18 +8,28 @@ const { quietLink, scheduleDeletion } = require('../../utils/tg');
|
||||
const { getAdmins } = require('../../stores/user');
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const staffHandler = async ctx => {
|
||||
const staffHandler = async (ctx) => {
|
||||
const admins = await getAdmins();
|
||||
admins.sort((a, b) => a.first_name > b.first_name ? 1 : -1);
|
||||
admins.sort((a, b) => (a.first_name > b.first_name ? 1 : -1));
|
||||
|
||||
const links = admins.map(quietLink);
|
||||
|
||||
const list = TgHtml.join('\n', links.map(s => html`⭐ ${s}`));
|
||||
const list = TgHtml.join(
|
||||
'\n',
|
||||
links.map((s) => html`⭐ ${s}`)
|
||||
);
|
||||
|
||||
return ctx.replyWithHTML(html`<b>Admins in the network:</b>\n\n${list}`, {
|
||||
disable_notification: true,
|
||||
disable_web_page_preview: true,
|
||||
}).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(
|
||||
html`<b>Admins in the network:</b>
|
||||
|
||||
${list}`,
|
||||
{
|
||||
disable_notification: true,
|
||||
disable_web_page_preview: true,
|
||||
}
|
||||
)
|
||||
.then(scheduleDeletion());
|
||||
};
|
||||
|
||||
module.exports = staffHandler;
|
||||
|
@ -17,14 +17,16 @@ const noop = Function.prototype;
|
||||
|
||||
const tgUnadmin = async (userToUnadmin) => {
|
||||
for (const group of await listGroups()) {
|
||||
telegram.promoteChatMember(group.id, userToUnadmin.id, {
|
||||
can_change_info: false,
|
||||
can_delete_messages: false,
|
||||
can_invite_users: false,
|
||||
can_pin_messages: false,
|
||||
can_promote_members: false,
|
||||
can_restrict_members: false,
|
||||
}).catch(noop);
|
||||
telegram
|
||||
.promoteChatMember(group.id, userToUnadmin.id, {
|
||||
can_change_info: false,
|
||||
can_delete_messages: false,
|
||||
can_invite_users: false,
|
||||
can_pin_messages: false,
|
||||
can_promote_members: false,
|
||||
can_restrict_members: false,
|
||||
})
|
||||
.catch(noop);
|
||||
}
|
||||
};
|
||||
|
||||
@ -35,22 +37,22 @@ const unAdminHandler = async (ctx) => {
|
||||
const { targets } = parse(ctx.message);
|
||||
|
||||
if (targets.length !== 1) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify one user to unadmin.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify one user to unadmin.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const userToUnadmin = await getUser(strip(targets[0]));
|
||||
|
||||
if (!userToUnadmin) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('❓ <b>User unknown.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (userToUnadmin.status !== 'admin') {
|
||||
return ctx.replyWithHTML(
|
||||
html`ℹ️ ${link(userToUnadmin)} <b>is not admin.</b>`,
|
||||
html`ℹ️ ${link(userToUnadmin)} <b>is not admin.</b>`
|
||||
);
|
||||
}
|
||||
|
||||
@ -59,7 +61,7 @@ const unAdminHandler = async (ctx) => {
|
||||
await unadmin(userToUnadmin);
|
||||
|
||||
return ctx.replyWithHTML(
|
||||
html`❗️ ${link(userToUnadmin)} <b>is no longer admin.</b>`,
|
||||
html`❗️ ${link(userToUnadmin)} <b>is no longer admin.</b>`
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -14,45 +14,50 @@ const { getUser, unban } = require('../../stores/user');
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const unbanHandler = async (ctx) => {
|
||||
if (ctx.from?.status !== 'admin') return null;
|
||||
if (typeof ctx.message === 'undefined') return null;
|
||||
if (!('text' in ctx.message)) return null;
|
||||
|
||||
const { targets } = parse(ctx.message);
|
||||
|
||||
if (targets.length !== 1) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify one user to unban.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify one user to unban.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const userToUnban = await getUser(strip(targets[0]));
|
||||
|
||||
if (!userToUnban) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('❓ <b>User unknown.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
|
||||
if (userToUnban.status !== 'banned') {
|
||||
return ctx.replyWithHTML('ℹ️ <b>User is not banned.</b>');
|
||||
}
|
||||
|
||||
await pMap(await listGroups({ type: 'supergroup' }), (group) =>
|
||||
ctx.telegram.unbanChatMember(group.id, userToUnban.id));
|
||||
ctx.telegram.unbanChatMember(group.id, userToUnban.id)
|
||||
);
|
||||
|
||||
await unban(userToUnban);
|
||||
|
||||
ctx.telegram.sendMessage(
|
||||
userToUnban.id,
|
||||
'♻️ You were unbanned from all of the /groups!',
|
||||
).catch(() => null);
|
||||
ctx.telegram
|
||||
.sendMessage(
|
||||
userToUnban.id,
|
||||
'♻️ You were unbanned from all of the /groups!'
|
||||
)
|
||||
.catch(() => null);
|
||||
// it's likely that the banned person haven't PMed the bot,
|
||||
// which will cause the sendMessage to fail,
|
||||
// hance .catch(noop)
|
||||
// (it's an expected, non-critical failure)
|
||||
|
||||
|
||||
return ctx.loggedReply(html`
|
||||
♻️ ${lrm}${ctx.from.first_name} <b>unbanned</b> ${displayUser(userToUnban)}.
|
||||
♻️ ${lrm}${ctx.from.first_name} <b>unbanned</b> ${displayUser(
|
||||
userToUnban
|
||||
)}.
|
||||
`);
|
||||
};
|
||||
|
||||
|
@ -32,34 +32,37 @@ $`;
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const unwarnHandler = async (ctx) => {
|
||||
if (ctx.from?.status !== 'admin') return null;
|
||||
if (typeof ctx.message === 'undefined') return null;
|
||||
if (!('text' in ctx.message)) return null;
|
||||
|
||||
const { reason, targets } = parse(ctx.message);
|
||||
|
||||
if (targets.length !== 1) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify one user to unwarn.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify one user to unwarn.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const userToUnwarn = await getUser(strip(targets[0]));
|
||||
|
||||
if (!userToUnwarn) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('❓ <b>User unknown</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const allWarns = userToUnwarn.warns.filter(isWarnNotExpired(new Date()));
|
||||
|
||||
if (allWarns.length === 0) {
|
||||
return ctx.replyWithHTML(
|
||||
html`ℹ️ ${link(userToUnwarn)} <b>already has no warnings.</b>`,
|
||||
html`ℹ️ ${link(userToUnwarn)} <b>already has no warnings.</b>`
|
||||
);
|
||||
}
|
||||
|
||||
if (userToUnwarn.status === 'banned') {
|
||||
await pMap(await listGroups({ type: 'supergroup' }), group =>
|
||||
ctx.telegram.unbanChatMember(group.id, userToUnwarn.id));
|
||||
await pMap(await listGroups({ type: 'supergroup' }), (group) =>
|
||||
ctx.telegram.unbanChatMember(group.id, userToUnwarn.id)
|
||||
);
|
||||
}
|
||||
|
||||
let lastWarn;
|
||||
@ -67,27 +70,30 @@ const unwarnHandler = async (ctx) => {
|
||||
lastWarn = last(allWarns);
|
||||
} else if (dateRegex.test(reason)) {
|
||||
const normalized = reason.replace(' ', 'T').toUpperCase();
|
||||
lastWarn = allWarns.find(({ date }) =>
|
||||
date && date.toISOString().startsWith(normalized));
|
||||
lastWarn = allWarns.find(
|
||||
({ date }) => date && date.toISOString().startsWith(normalized)
|
||||
);
|
||||
} else {
|
||||
return ctx.replyWithHTML(
|
||||
'⚠ <b>Invalid date</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('⚠ <b>Invalid date</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (!lastWarn) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>404: Warn not found</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('❓ <b>404: Warn not found</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
await unwarn(userToUnwarn, lastWarn);
|
||||
|
||||
if (userToUnwarn.status === 'banned') {
|
||||
ctx.telegram.sendMessage(
|
||||
userToUnwarn.id,
|
||||
'♻️ You were unbanned from all of the /groups!',
|
||||
).catch(() => null);
|
||||
ctx.telegram
|
||||
.sendMessage(
|
||||
userToUnwarn.id,
|
||||
'♻️ You were unbanned from all of the /groups!'
|
||||
)
|
||||
.catch(() => null);
|
||||
// it's likely that the banned person haven't PMed the bot,
|
||||
// which will cause the sendMessage to fail,
|
||||
// hance .catch(noop)
|
||||
@ -97,10 +103,9 @@ const unwarnHandler = async (ctx) => {
|
||||
const count = html`<b>${allWarns.length}</b>/${numberOfWarnsToBan}`;
|
||||
|
||||
return ctx.loggedReply(html`
|
||||
❎ ${lrm}${ctx.from.first_name} <b>pardoned</b> ${link(userToUnwarn)} for
|
||||
${count}: ${lrm}${lastWarn.reason || lastWarn}
|
||||
❎ ${lrm}${ctx.from.first_name} <b>pardoned</b> ${link(userToUnwarn)}
|
||||
for ${count}: ${lrm}${lastWarn.reason || lastWarn}
|
||||
`);
|
||||
};
|
||||
|
||||
|
||||
module.exports = unwarnHandler;
|
||||
|
@ -12,7 +12,7 @@ const { parse, strip } = require('../../utils/cmd');
|
||||
const { getUser, permit } = require('../../stores/user');
|
||||
|
||||
/** @param {Date} date */
|
||||
const formatDate = date =>
|
||||
const formatDate = (date) =>
|
||||
date && date.toISOString().slice(0, -5).replace('T', ' ') + ' UTC';
|
||||
|
||||
/**
|
||||
@ -20,8 +20,9 @@ const formatDate = date =>
|
||||
*/
|
||||
const formatEntry = async (entry, defaultVal) => {
|
||||
if (!entry || !entry.by_id) return html`${defaultVal}`;
|
||||
const { first_name } = await getUser({ id: entry.by_id }) || {};
|
||||
if (!first_name) return html`${lrm}${entry.reason} (${formatDate(entry.date)})`;
|
||||
const { first_name } = (await getUser({ id: entry.by_id })) || {};
|
||||
if (!first_name)
|
||||
return html`${lrm}${entry.reason} (${formatDate(entry.date)})`;
|
||||
return html`${lrm}${entry.reason} (${first_name}, ${formatDate(entry.date)})`;
|
||||
};
|
||||
|
||||
@ -33,14 +34,12 @@ const formatWarn = async (warn, i) =>
|
||||
/**
|
||||
* @param {string | TgHtml | undefined } content
|
||||
*/
|
||||
const isNotEmpty = content => content?.length;
|
||||
const isNotEmpty = (content) => content?.length;
|
||||
|
||||
const optional = (header, sep, content) =>
|
||||
isNotEmpty(content)
|
||||
? html`${header}${sep}${content}`
|
||||
: html``;
|
||||
isNotEmpty(content) ? html`${header}${sep}${content}` : html``;
|
||||
|
||||
const title = user => {
|
||||
const title = (user) => {
|
||||
if (isMaster(user)) {
|
||||
return html`🕴️ <b>Bot master</b>`;
|
||||
} else if (user.status === 'admin') {
|
||||
@ -52,70 +51,80 @@ const title = user => {
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const getWarnsHandler = async (ctx) => {
|
||||
if (!ctx.from) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>This command is not available in channels.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(
|
||||
'ℹ️ <b>This command is not available in channels.</b>'
|
||||
)
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (typeof ctx.message === 'undefined') return;
|
||||
if (!('text' in ctx.message)) return;
|
||||
|
||||
const { flags, targets } = parse(ctx.message);
|
||||
|
||||
if (targets.length > 1) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify one user.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify one user.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const theUser = targets.length && ctx.from.status === 'admin'
|
||||
? await getUser(strip(targets[0]))
|
||||
: ctx.from;
|
||||
const theUser =
|
||||
targets.length && ctx.from.status === 'admin'
|
||||
? await getUser(strip(targets[0]))
|
||||
: ctx.from;
|
||||
|
||||
if (!theUser) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('❓ <b>User unknown.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (flags.has('raw') && ctx.from.status === 'admin') {
|
||||
return ctx.replyWithHTML(
|
||||
TgHtml.pre(inspect(theUser)),
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(TgHtml.pre(inspect(theUser)))
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const header = html`${title(theUser)} ${displayUser(theUser)}`;
|
||||
const banReason = optional(
|
||||
html`🚫 <b>Ban reason:</b>`,
|
||||
' ',
|
||||
await formatEntry(theUser.ban_details, theUser.ban_reason || ''),
|
||||
await formatEntry(theUser.ban_details, theUser.ban_reason || '')
|
||||
);
|
||||
const { warns = [] } = theUser;
|
||||
const userWarns = optional(
|
||||
html`<b>⚠️ Warns:</b>`,
|
||||
'\n',
|
||||
TgHtml.join('\n', await Promise.all(warns.map(formatWarn))),
|
||||
TgHtml.join('\n', await Promise.all(warns.map(formatWarn)))
|
||||
);
|
||||
|
||||
const firstSeen = optional(
|
||||
html`👀 <b>First seen:</b>`,
|
||||
' ',
|
||||
formatDate(theUser.createdAt),
|
||||
formatDate(theUser.createdAt)
|
||||
);
|
||||
|
||||
const permits = permit.isValid(theUser.permit)
|
||||
// eslint-disable-next-line max-len
|
||||
? `🎟 ${(await getUser({ id: theUser.permit.by_id })).first_name}, ${formatDate(theUser.permit.date)}`
|
||||
? // eslint-disable-next-line max-len
|
||||
`🎟 ${
|
||||
(await getUser({ id: theUser.permit.by_id })).first_name
|
||||
}, ${formatDate(theUser.permit.date)}`
|
||||
: '';
|
||||
|
||||
const oneliners = TgHtml.join('\n', [
|
||||
header,
|
||||
firstSeen,
|
||||
permits,
|
||||
].filter(isNotEmpty));
|
||||
const oneliners = TgHtml.join(
|
||||
'\n',
|
||||
[header, firstSeen, permits].filter(isNotEmpty)
|
||||
);
|
||||
|
||||
return ctx.replyWithHTML(TgHtml.join('\n\n', [
|
||||
oneliners,
|
||||
userWarns,
|
||||
banReason,
|
||||
].filter(isNotEmpty))).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(
|
||||
TgHtml.join(
|
||||
'\n\n',
|
||||
[oneliners, userWarns, banReason].filter(isNotEmpty)
|
||||
)
|
||||
)
|
||||
.then(scheduleDeletion());
|
||||
};
|
||||
|
||||
module.exports = getWarnsHandler;
|
||||
|
@ -11,7 +11,7 @@ const { getUser } = require('../../stores/user');
|
||||
const warnHandler = async (ctx) => {
|
||||
if (!ctx.message.chat.type.endsWith('group')) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>This command is only available in groups.</b>',
|
||||
'ℹ️ <b>This command is only available in groups.</b>'
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,40 +20,44 @@ const warnHandler = async (ctx) => {
|
||||
const { flags, reason, targets } = parse(ctx.message);
|
||||
|
||||
if (targets.length !== 1) {
|
||||
return ctx.replyWithHTML(
|
||||
'ℹ️ <b>Specify one user to warn.</b>',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Specify one user to warn.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
const userToWarn = await getUser(strip(targets[0]));
|
||||
|
||||
if (!userToWarn) {
|
||||
return ctx.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>\n' +
|
||||
'Please forward their message, then try again.',
|
||||
).then(scheduleDeletion());
|
||||
return ctx
|
||||
.replyWithHTML(
|
||||
'❓ <b>User unknown.</b>\n' +
|
||||
'Please forward their message, then try again.'
|
||||
)
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (userToWarn.id === ctx.botInfo.id) return null;
|
||||
|
||||
if (userToWarn.status === 'admin') {
|
||||
return ctx.replyWithHTML('ℹ️ <b>Can\'t warn other admins.</b>');
|
||||
return ctx.replyWithHTML("ℹ️ <b>Can't warn other admins.</b>");
|
||||
}
|
||||
|
||||
if (reason.length === 0) {
|
||||
return ctx.replyWithHTML('ℹ️ <b>Need a reason to warn.</b>')
|
||||
return ctx
|
||||
.replyWithHTML('ℹ️ <b>Need a reason to warn.</b>')
|
||||
.then(scheduleDeletion());
|
||||
}
|
||||
|
||||
if (ctx.message.reply_to_message) {
|
||||
ctx.deleteMessage(ctx.message.reply_to_message.message_id)
|
||||
.catch(() => null);
|
||||
ctx.deleteMessage(ctx.message.reply_to_message.message_id).catch(
|
||||
() => null
|
||||
);
|
||||
}
|
||||
|
||||
return ctx.warn({
|
||||
admin: ctx.from,
|
||||
amend: flags.has('amend'),
|
||||
reason: '[' + ctx.chat.title + '] ' + await substom(reason),
|
||||
reason: '[' + ctx.chat.title + '] ' + (await substom(reason)),
|
||||
userToWarn,
|
||||
mode: 'manual',
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Context, Middleware } from "telegraf";
|
||||
import { addGroup } from "../../stores/group";
|
||||
import { admin } from "../../stores/user";
|
||||
import { isMaster } from "../../utils/config";
|
||||
import { Context, Middleware } from 'telegraf';
|
||||
import { addGroup } from '../../stores/group';
|
||||
import { admin } from '../../stores/user';
|
||||
import { isMaster } from '../../utils/config';
|
||||
|
||||
const addedToGroupHandler: Middleware<Context> = async (ctx, next) => {
|
||||
const wasAdded = ctx.message?.new_chat_members?.some(
|
||||
@ -11,15 +11,15 @@ const addedToGroupHandler: Middleware<Context> = async (ctx, next) => {
|
||||
await admin(ctx.from!);
|
||||
const link = ctx.chat!.username
|
||||
? `https://t.me/${ctx.chat!.username.toLowerCase()}`
|
||||
: await ctx.exportChatInviteLink().catch(() => "");
|
||||
: await ctx.exportChatInviteLink().catch(() => '');
|
||||
if (!link) {
|
||||
await ctx.replyWithHTML(
|
||||
"⚠️ <b>Failed to export chat invite link.</b>\n" +
|
||||
'⚠️ <b>Failed to export chat invite link.</b>\n' +
|
||||
"Group won't be visible in /groups list.\n" +
|
||||
"\n" +
|
||||
'\n' +
|
||||
"If this isn't your intention, " +
|
||||
"make sure I am permitted to export chat invite link, " +
|
||||
"and then run /showgroup."
|
||||
'make sure I am permitted to export chat invite link, ' +
|
||||
'and then run /showgroup.'
|
||||
);
|
||||
}
|
||||
const { id, title, type } = ctx.chat!;
|
||||
|
@ -2,14 +2,15 @@
|
||||
|
||||
const { pMap } = require('../../utils/promise');
|
||||
|
||||
const link = user => '@' + user.username;
|
||||
const link = (user) => '@' + user.username;
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const antibotHandler = async (ctx, next) => {
|
||||
const msg = ctx.message;
|
||||
|
||||
const bots = msg.new_chat_members.filter(user =>
|
||||
user.is_bot && user.username !== ctx.me);
|
||||
const bots = msg.new_chat_members.filter(
|
||||
(user) => user.is_bot && user.username !== ctx.me
|
||||
);
|
||||
|
||||
if (bots.length === 0) {
|
||||
return next();
|
||||
@ -19,11 +20,10 @@ const antibotHandler = async (ctx, next) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
await pMap(bots, bot =>
|
||||
ctx.kickChatMember(bot.id));
|
||||
await pMap(bots, (bot) => ctx.banChatMember(bot.id));
|
||||
|
||||
await ctx.replyWithHTML(
|
||||
`🚫 <b>Kicked bot(s):</b> ${bots.map(link).join(', ')}`,
|
||||
`🚫 <b>Kicked bot(s):</b> ${bots.map(link).join(', ')}`
|
||||
);
|
||||
|
||||
return next();
|
||||
|
@ -1,17 +1,17 @@
|
||||
/* eslint new-cap: ["error", {"capIsNewExceptionPattern": "^(?:Action|jspack)\."}] */
|
||||
|
||||
import * as R from "ramda";
|
||||
import { html, lrm } from "../../utils/html";
|
||||
import { isAdmin, permit } from "../../stores/user";
|
||||
import { config } from "../../utils/config";
|
||||
import type { ExtendedContext } from "../../typings/context";
|
||||
import { jspack } from "jspack";
|
||||
import { managesGroup } from "../../stores/group";
|
||||
import type { MessageEntity } from "telegraf/typings/telegram-types";
|
||||
import { pMap } from "../../utils/promise";
|
||||
import { telegram } from "../../bot";
|
||||
import { URL } from "url";
|
||||
import XRegExp = require("xregexp");
|
||||
import * as R from 'ramda';
|
||||
import { html, lrm } from '../../utils/html';
|
||||
import { isAdmin, permit } from '../../stores/user';
|
||||
import { config } from '../../utils/config';
|
||||
import type { ExtendedContext } from '../../typings/context';
|
||||
import { jspack } from 'jspack';
|
||||
import { managesGroup } from '../../stores/group';
|
||||
import type { MessageEntity } from 'telegraf/typings/telegram-types';
|
||||
import { pMap } from '../../utils/promise';
|
||||
import { telegram } from '../../bot';
|
||||
import { URL } from 'url';
|
||||
import XRegExp = require('xregexp');
|
||||
|
||||
const { excludeLinks = [], blacklistedDomains = [] } = config;
|
||||
const { fetch } = require('undici');
|
||||
@ -22,19 +22,19 @@ if (excludeLinks === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tmeDomains = ["t.me", "telega.one", "telegram.dog", "telegram.me"];
|
||||
const tmeDomains = ['t.me', 'telega.one', 'telegram.dog', 'telegram.me'];
|
||||
|
||||
const tmeDomainRegex = XRegExp.union(tmeDomains);
|
||||
|
||||
const normalizeTme = R.replace(
|
||||
XRegExp.tag("i")`^(?:@|(?:https?:\/\/)?${tmeDomainRegex}\/)(\w+)(\/.+)?`,
|
||||
XRegExp.tag('i')`^(?:@|(?:https?:\/\/)?${tmeDomainRegex}\/)(\w+)(\/.+)?`,
|
||||
(_match, username, rest) =>
|
||||
/^\/\d+$/.test(rest)
|
||||
? `https://t.me/${username.toLowerCase()}`
|
||||
: `https://t.me/${username.toLowerCase()}${rest || ""}`
|
||||
: `https://t.me/${username.toLowerCase()}${rest || ''}`
|
||||
);
|
||||
|
||||
const stripQuery = (s: string) => s.split("?", 1)[0];
|
||||
const stripQuery = (s: string) => s.split('?', 1)[0];
|
||||
|
||||
const customWhitelist = new Set(excludeLinks.map(normalizeTme).map(stripQuery));
|
||||
|
||||
@ -65,35 +65,38 @@ type Action = Action.Nothing | Action.Notify | Action.Warn;
|
||||
|
||||
const actionPriority = (action: Action) => action.type;
|
||||
const maxByActionPriority = R.maxBy(actionPriority);
|
||||
const highestPriorityAction = R.reduce(maxByActionPriority, Action.Nothing);
|
||||
const highestPriorityAction = (arr: Action[]) =>
|
||||
arr.reduce(maxByActionPriority, Action.Nothing);
|
||||
|
||||
const assumeProtocol = R.unless(R.contains("://"), R.concat("http://"));
|
||||
const isHttp = R.propSatisfies(R.test(/^https?:$/i), "protocol");
|
||||
const assumeProtocol = R.unless(R.includes('://'), (s) => `http://${s}`);
|
||||
const isHttp = R.propSatisfies(R.test(/^https?:$/i), 'protocol');
|
||||
const isLink = (entity: MessageEntity) =>
|
||||
["url", "text_link", "mention"].includes(entity.type);
|
||||
['url', 'text_link', 'mention'].includes(entity.type);
|
||||
|
||||
const obtainUrlFromText = (text: string) => ({ length, offset, url = "" }) =>
|
||||
url ? url : text.slice(offset, length + offset);
|
||||
const obtainUrlFromText =
|
||||
(text: string) =>
|
||||
({ length, offset, url = '' }) =>
|
||||
url ? url : text.slice(offset, length + offset);
|
||||
|
||||
const blacklisted = {
|
||||
protocol: (url: URL) =>
|
||||
url.protocol === "tg:" && url.host.toLowerCase() === "resolve",
|
||||
url.protocol === 'tg:' && url.host.toLowerCase() === 'resolve',
|
||||
};
|
||||
|
||||
const isPublic = async (username: string) => {
|
||||
try {
|
||||
const chat = await telegram.getChat(username);
|
||||
return chat.type !== "private";
|
||||
return chat.type !== 'private';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const inviteLinkToGroupID = (url: URL) => {
|
||||
if (url.pathname.toLowerCase().startsWith("/joinchat/")) {
|
||||
if (url.pathname.toLowerCase().startsWith('/joinchat/')) {
|
||||
const res = jspack.Unpack(
|
||||
">LLQ",
|
||||
Buffer.from(url.pathname.split("/")[2], "base64")
|
||||
'>LLQ',
|
||||
Buffer.from(url.pathname.split('/')[2], 'base64')
|
||||
);
|
||||
if (Array.isArray(res)) {
|
||||
const [, groupID] = res;
|
||||
@ -112,24 +115,26 @@ const managesLinkedGroup = (url: URL) => {
|
||||
|
||||
const dh = {
|
||||
blacklistedDomain: R.always(
|
||||
Promise.resolve(Action.Warn("Link to a blacklisted domain"))
|
||||
Promise.resolve(Action.Warn('Link to a blacklisted domain'))
|
||||
),
|
||||
nothing: R.always(Promise.resolve(Action.Nothing)),
|
||||
tme: async (url: URL) => {
|
||||
if (url.pathname === "/") return Action.Nothing;
|
||||
if (url.pathname.toLowerCase().startsWith("/c/")) return Action.Nothing;
|
||||
if (url.pathname.toLowerCase().startsWith("/addtheme/")) return Action.Nothing;
|
||||
if (url.pathname.toLowerCase().startsWith("/addstickers/")) {
|
||||
if (url.pathname === '/') return Action.Nothing;
|
||||
if (url.pathname.toLowerCase().startsWith('/c/')) return Action.Nothing;
|
||||
if (url.pathname.toLowerCase().startsWith('/addtheme/'))
|
||||
return Action.Nothing;
|
||||
if (url.pathname.toLowerCase().startsWith('/addstickers/')) {
|
||||
return Action.Nothing;
|
||||
}
|
||||
if (url.pathname.toLowerCase().startsWith("/setlanguage/")) {
|
||||
if (url.pathname.toLowerCase().startsWith('/setlanguage/')) {
|
||||
return Action.Nothing;
|
||||
}
|
||||
if (url.searchParams.has("start")) return Action.Warn("Bot reflink");
|
||||
if (url.searchParams.has('start')) return Action.Warn('Bot reflink');
|
||||
if (await managesLinkedGroup(url)) return Action.Nothing;
|
||||
const [, username] = R.match(/^\/(\w+)(?:\/\d*)?$/, url.pathname);
|
||||
if (username && !(await isPublic("@" + username))) return Action.Nothing;
|
||||
return Action.Warn("Link to a Telegram group or channel");
|
||||
if (username && !(await isPublic('@' + username)))
|
||||
return Action.Nothing;
|
||||
return Action.Warn('Link to a Telegram group or channel');
|
||||
},
|
||||
};
|
||||
|
||||
@ -144,13 +149,14 @@ const isWhitelisted = (url: URL) =>
|
||||
customWhitelist.has(stripQuery(url.toString()));
|
||||
|
||||
class CodeError extends Error {
|
||||
url?: URL;
|
||||
constructor(readonly code: string) {
|
||||
super(code);
|
||||
}
|
||||
}
|
||||
|
||||
const unshorten = (url: URL | string) =>
|
||||
fetch(url, { redirect: "follow" }).then((res) =>
|
||||
fetch(url, { redirect: 'follow' }).then((res) =>
|
||||
res.ok
|
||||
? new URL(normalizeTme(res.url))
|
||||
: Promise.reject(new CodeError(`${res.status} ${res.statusText}`))
|
||||
@ -162,43 +168,49 @@ const checkLinkByDomain = (url: URL) => {
|
||||
return handler(url);
|
||||
};
|
||||
|
||||
const classifyAsync = R.memoize(async (url: URL) => {
|
||||
if (isWhitelisted(url)) return Action.Nothing;
|
||||
const classifyAsync = R.memoizeWith(
|
||||
(key) => key.href,
|
||||
async (url: URL) => {
|
||||
if (isWhitelisted(url)) return Action.Nothing;
|
||||
|
||||
if (blacklisted.protocol(url)) return Action.Warn("Link using tg protocol");
|
||||
if (blacklisted.protocol(url))
|
||||
return Action.Warn('Link using tg protocol');
|
||||
|
||||
if (domainHandlers.has(url.host.toLowerCase())) return checkLinkByDomain(url);
|
||||
if (domainHandlers.has(url.host.toLowerCase()))
|
||||
return checkLinkByDomain(url);
|
||||
|
||||
if (!isHttp(url)) return Action.Nothing;
|
||||
if (!isHttp(url)) return Action.Nothing;
|
||||
|
||||
try {
|
||||
const longUrl = await unshorten(url);
|
||||
if (isWhitelisted(longUrl)) return Action.Nothing;
|
||||
return checkLinkByDomain(longUrl);
|
||||
} catch (e) {
|
||||
e.url = url;
|
||||
return Action.Notify(e);
|
||||
try {
|
||||
const longUrl = await unshorten(url);
|
||||
if (isWhitelisted(longUrl)) return Action.Nothing;
|
||||
return checkLinkByDomain(longUrl);
|
||||
} catch (_e) {
|
||||
const e = _e as CodeError;
|
||||
e.url = url;
|
||||
return Action.Notify(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const classifyList = (urls: URL[]) =>
|
||||
pMap(urls, classifyAsync).then(highestPriorityAction);
|
||||
|
||||
const matchTmeLinks = R.match(XRegExp.tag("gi")`\b${tmeDomainRegex}\/[\w-/]+`);
|
||||
const matchTmeLinks = R.match(XRegExp.tag('gi')`\b${tmeDomainRegex}\/[\w-/]+`);
|
||||
|
||||
const maybeProp = (prop) => (o) => (R.has(prop, o) ? [o[prop]] : []);
|
||||
const maybeProp = (prop) => (o) => R.has(prop, o) ? [o[prop]] : [];
|
||||
|
||||
const buttonUrls = R.pipe(
|
||||
R.path(["reply_markup", "inline_keyboard"]),
|
||||
R.path(['reply_markup', 'inline_keyboard']),
|
||||
R.defaultTo([]),
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
R.unnest,
|
||||
R.chain(maybeProp("url"))
|
||||
R.chain(maybeProp('url'))
|
||||
);
|
||||
|
||||
const classifyCtx = (ctx: ExtendedContext) => {
|
||||
if (!ctx.chat?.type.endsWith("group")) return Action.Nothing;
|
||||
if (!ctx.chat?.type.endsWith('group')) return Action.Nothing;
|
||||
|
||||
const message = ctx.message || ctx.editedMessage;
|
||||
|
||||
@ -206,7 +218,7 @@ const classifyCtx = (ctx: ExtendedContext) => {
|
||||
|
||||
const entities = message.entities || message.caption_entities || [];
|
||||
|
||||
const text = message.text || message.caption || "";
|
||||
const text = message.text || message.caption || '';
|
||||
|
||||
const rawUrls = entities
|
||||
.filter(isLink)
|
||||
@ -240,10 +252,10 @@ export = async (ctx: ExtendedContext, next) => {
|
||||
|
||||
ctx.deleteMessage().catch(() => null);
|
||||
return ctx.warn({
|
||||
admin: ctx.botInfo!,
|
||||
admin: ctx.botInfo,
|
||||
reason: action.reason,
|
||||
userToWarn,
|
||||
mode: "auto",
|
||||
mode: 'auto',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ module.exports = (ctx, next) => {
|
||||
from: ctx.from,
|
||||
chat: ctx.chat,
|
||||
text: ctx.callbackQuery.data,
|
||||
entities: [ { offset: 0, type: 'bot_command' } ],
|
||||
entities: [{ offset: 0, type: 'bot_command' }],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -37,17 +37,17 @@ composer.use(updateUserDataHandler);
|
||||
composer.on('new_chat_members', syncStatusHandler, antibotHandler);
|
||||
composer.on('message', kickBannedHandler);
|
||||
composer.use(removeChannelForwardsHandler);
|
||||
composer.on([ 'edited_message', 'message' ], checkLinksHandler);
|
||||
composer.on(['edited_message', 'message'], checkLinksHandler);
|
||||
composer.on('new_chat_title', updateGroupTitleHandler);
|
||||
composer.on('text', removeCommandsHandler);
|
||||
composer.on(
|
||||
[ 'new_chat_members', 'left_chat_member' ],
|
||||
['new_chat_members', 'left_chat_member'],
|
||||
deleteAfter(deleteJoinsAfter),
|
||||
presenceLogHandler,
|
||||
presenceLogHandler
|
||||
);
|
||||
composer.action(
|
||||
/^\/del -chat_id=(-\d+) -msg_id=(\d+) Report handled/,
|
||||
reportHandled,
|
||||
reportHandled
|
||||
);
|
||||
composer.on('callback_query', commandButtons);
|
||||
|
||||
|
@ -9,8 +9,9 @@ const kickBannedHandler = (ctx, next) => {
|
||||
}
|
||||
if (ctx.from.status === 'banned') {
|
||||
ctx.deleteMessage().catch(noop);
|
||||
return ctx.kickChatMember(ctx.from.id)
|
||||
.catch(err => ctx.reply(`⚠️ kickBanned: ${err}`));
|
||||
return ctx
|
||||
.banChatMember(ctx.from.id)
|
||||
.catch((err) => ctx.reply(`⚠️ kickBanned: ${err}`));
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
@ -6,7 +6,6 @@ const { chats = {} } = require('../../utils/config').config;
|
||||
|
||||
const pkg = require('../../package.json');
|
||||
|
||||
|
||||
const caption = `\
|
||||
Sorry, you need to set up your own instance \
|
||||
to use me in your group or a network of groups.
|
||||
@ -15,10 +14,14 @@ If you don't wish to self host, \
|
||||
you can try @MissRose_bot instead.
|
||||
`;
|
||||
|
||||
const inline_keyboard = [ [ {
|
||||
text: '🛠 Setup a New Bot',
|
||||
url: pkg.homepage,
|
||||
} ] ];
|
||||
const inline_keyboard = [
|
||||
[
|
||||
{
|
||||
text: '🛠 Setup a New Bot',
|
||||
url: pkg.homepage,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const reply_markup = JSON.stringify({ inline_keyboard });
|
||||
|
||||
@ -30,15 +33,13 @@ const gifIds = [
|
||||
'3XiQswSmbjBiU',
|
||||
];
|
||||
|
||||
const gifs = gifIds.map(x => `https://media.giphy.com/media/${x}/giphy.gif`);
|
||||
|
||||
const gifs = gifIds.map((x) => `https://media.giphy.com/media/${x}/giphy.gif`);
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} arr
|
||||
*/
|
||||
const randomChoice = arr => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
const randomChoice = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
/** @param { import('telegraf').Context } ctx */
|
||||
const leaveUnmanagedHandler = async (ctx, next) => {
|
||||
@ -50,7 +51,8 @@ const leaveUnmanagedHandler = async (ctx, next) => {
|
||||
if (
|
||||
ctx.chat.type === 'private' ||
|
||||
Object.values(chats).includes(ctx.chat.id) ||
|
||||
await managesGroup({ id: ctx.chat.id })) {
|
||||
(await managesGroup({ id: ctx.chat.id }))
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const R = require('ramda');
|
||||
const { Telegraf: { optional, passThru } } = require('telegraf');
|
||||
const {
|
||||
Telegraf: { optional, passThru },
|
||||
} = require('telegraf');
|
||||
const { permit } = require('../../stores/user');
|
||||
|
||||
const { html, lrm } = require('../../utils/html');
|
||||
@ -13,29 +15,29 @@ if (excludeLinks === false || excludeLinks === '*') {
|
||||
}
|
||||
|
||||
const isChannelForward = R.pathEq(
|
||||
[ 'message', 'forward_from_chat', 'type' ],
|
||||
'channel',
|
||||
['message', 'forward_from_chat', 'type'],
|
||||
'channel'
|
||||
);
|
||||
const fromAdmin = R.pathEq([ 'from', 'status' ], 'admin');
|
||||
const fromAdmin = R.pathEq(['from', 'status'], 'admin');
|
||||
|
||||
const inGroup = ctx => ctx.chat?.type.endsWith('group');
|
||||
const inGroup = (ctx) => ctx.chat?.type.endsWith('group');
|
||||
|
||||
const capturingGroups = R.tail;
|
||||
|
||||
const toUsername = R.compose(
|
||||
capturingGroups,
|
||||
R.match(/^(?:@|(?:https?:\/\/)?(?:t\.me|telegram\.(?:me|dog))\/)(\w+)/i),
|
||||
R.match(/^(?:@|(?:https?:\/\/)?(?:t\.me|telegram\.(?:me|dog))\/)(\w+)/i)
|
||||
);
|
||||
|
||||
const customWhitelist = R.pipe(
|
||||
R.chain(toUsername),
|
||||
R.map(R.toLower),
|
||||
R.constructN(1, Set),
|
||||
R.constructN(1, Set)
|
||||
)(excludeLinks);
|
||||
|
||||
const isWhitelisted = username => customWhitelist.has(username.toLowerCase());
|
||||
const isWhitelisted = (username) => customWhitelist.has(username.toLowerCase());
|
||||
|
||||
const fromWhitelisted = ctx =>
|
||||
const fromWhitelisted = (ctx) =>
|
||||
isWhitelisted(ctx.message.forward_from_chat.username || '');
|
||||
|
||||
const pred = R.allPass([
|
||||
@ -48,7 +50,9 @@ const pred = R.allPass([
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const handler = async (ctx, next) => {
|
||||
if (await permit.revoke(ctx.from)) {
|
||||
await ctx.replyWithHTML(html`${lrm}${ctx.from.first_name} used 🎟 permit!`);
|
||||
await ctx.replyWithHTML(
|
||||
html`${lrm}${ctx.from.first_name} used 🎟 permit!`
|
||||
);
|
||||
return next();
|
||||
}
|
||||
|
||||
|
@ -7,12 +7,13 @@ const { unmatched } = require('../unmatched');
|
||||
const shouldDelete = {
|
||||
all: () => true,
|
||||
none: () => false,
|
||||
own: ctx => !ctx.state[unmatched],
|
||||
own: (ctx) => !ctx.state[unmatched],
|
||||
};
|
||||
|
||||
if (!(deleteCommands in shouldDelete)) {
|
||||
throw new Error('Invalid value for `deleteCommands` in config file: ' +
|
||||
deleteCommands);
|
||||
throw new Error(
|
||||
'Invalid value for `deleteCommands` in config file: ' + deleteCommands
|
||||
);
|
||||
}
|
||||
|
||||
const noop = Function.prototype;
|
||||
|
@ -1,11 +1,14 @@
|
||||
import type { ExtendedContext } from "../../typings/context";
|
||||
import type { ExtendedContext } from '../../typings/context';
|
||||
|
||||
export = (ctx: ExtendedContext) => {
|
||||
if (ctx.from?.status !== "admin") {
|
||||
return ctx.answerCbQuery("✋ Not permitted!", false, { cache_time: 600 });
|
||||
if (ctx.from?.status !== 'admin') {
|
||||
return ctx.answerCbQuery('✋ Not permitted!', {
|
||||
show_alert: false,
|
||||
cache_time: 600,
|
||||
});
|
||||
}
|
||||
|
||||
const [, chatId, msgId] = ctx.match!;
|
||||
const [, chatId, msgId] = ctx.callbackQuery?.inline_message_id!;
|
||||
|
||||
return Promise.all([
|
||||
ctx.deleteMessage(),
|
||||
|
@ -12,7 +12,7 @@ const { pMap } = require('../../utils/promise');
|
||||
const handleNewMember = async (ctx, newMember) => {
|
||||
if (await spamwatch.shouldKick(newMember)) {
|
||||
const until_date = (Date.now() + ms('24h')) / 1000;
|
||||
return ctx.kickChatMember(newMember.id, until_date);
|
||||
return ctx.banChatMember(newMember.id, until_date);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -20,7 +20,7 @@ const handleNewMember = async (ctx, newMember) => {
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const syncStatusHandler = (ctx, next) => {
|
||||
pMap(ctx.message.new_chat_members, async newMember => {
|
||||
pMap(ctx.message.new_chat_members, async (newMember) => {
|
||||
if (newMember.is_bot) {
|
||||
return null;
|
||||
}
|
||||
@ -29,21 +29,21 @@ const syncStatusHandler = (ctx, next) => {
|
||||
const { status = 'member' } = dbUser || {};
|
||||
|
||||
switch (status) {
|
||||
case 'admin':
|
||||
return ctx.promoteChatMember(newMember.id, {
|
||||
can_change_info: false,
|
||||
can_delete_messages: true,
|
||||
can_invite_users: true,
|
||||
can_pin_messages: true,
|
||||
can_promote_members: false,
|
||||
can_restrict_members: true,
|
||||
});
|
||||
case 'banned':
|
||||
return ctx.kickChatMember(newMember.id);
|
||||
case 'member':
|
||||
return handleNewMember(ctx, newMember);
|
||||
default:
|
||||
throw new Error(`Unexpected member status: ${dbUser.status}`);
|
||||
case 'admin':
|
||||
return ctx.promoteChatMember(newMember.id, {
|
||||
can_change_info: false,
|
||||
can_delete_messages: true,
|
||||
can_invite_users: true,
|
||||
can_pin_messages: true,
|
||||
can_promote_members: false,
|
||||
can_restrict_members: true,
|
||||
});
|
||||
case 'banned':
|
||||
return ctx.banChatMember(newMember.id);
|
||||
case 'member':
|
||||
return handleNewMember(ctx, newMember);
|
||||
default:
|
||||
throw new Error(`Unexpected member status: ${dbUser.status}`);
|
||||
}
|
||||
}).catch(() => null);
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const { Telegraf: { hears } } = require('telegraf');
|
||||
const {
|
||||
Telegraf: { hears },
|
||||
} = require('telegraf');
|
||||
const XRegExp = require('xregexp');
|
||||
|
||||
const { managesGroup } = require.main.require('./stores/group');
|
||||
@ -16,7 +18,7 @@ $`;
|
||||
|
||||
/** @param { import('../../typings/context').ExtendedContext } ctx */
|
||||
const handler = async (ctx, next) => {
|
||||
let [ , groupName ] = ctx.match;
|
||||
let [, groupName] = ctx.match;
|
||||
if (groupName.toLowerCase() === 'this') {
|
||||
if (!ctx.chat.title) return next();
|
||||
groupName = ctx.chat.title;
|
||||
|
@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const { Telegraf: { compose, hears } } = require('telegraf');
|
||||
const {
|
||||
Telegraf: { compose, hears },
|
||||
} = require('telegraf');
|
||||
|
||||
/* eslint-disable global-require */
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const { Telegraf: { hears } } = require('telegraf');
|
||||
const {
|
||||
Telegraf: { hears },
|
||||
} = require('telegraf');
|
||||
const R = require('ramda');
|
||||
|
||||
// DB
|
||||
@ -16,32 +18,30 @@ const deleteCustom = config.deleteCustom || { longerThan: Infinity };
|
||||
|
||||
const capitalize = R.replace(/^./, R.toUpper);
|
||||
|
||||
const getRepliedToId = R.path([ 'reply_to_message', 'message_id' ]);
|
||||
const getRepliedToId = R.path(['reply_to_message', 'message_id']);
|
||||
|
||||
const typeToMethod = type =>
|
||||
type === 'text'
|
||||
? 'replyWithHTML'
|
||||
: `replyWith${capitalize(type)}`;
|
||||
const typeToMethod = (type) =>
|
||||
type === 'text' ? 'replyWithHTML' : `replyWith${capitalize(type)}`;
|
||||
|
||||
const autoDelete = ({ content, type }) => {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return content.length > deleteCustom.longerThan;
|
||||
case 'copy':
|
||||
return (content.text || '').length > deleteCustom.longerThan;
|
||||
default:
|
||||
return false;
|
||||
case 'text':
|
||||
return content.length > deleteCustom.longerThan;
|
||||
case 'copy':
|
||||
return (content.text || '').length > deleteCustom.longerThan;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const hasRole = (role, from) => {
|
||||
switch (role.toLowerCase()) {
|
||||
case 'master':
|
||||
return isMaster(from);
|
||||
case 'admins':
|
||||
return from && from.status === 'admin';
|
||||
default:
|
||||
return true;
|
||||
case 'master':
|
||||
return isMaster(from);
|
||||
case 'admins':
|
||||
return from && from.status === 'admin';
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -57,17 +57,17 @@ const runCustomCmdHandler = async (ctx, next) => {
|
||||
const { caption, content, type } = command;
|
||||
|
||||
const options = {
|
||||
...caption && { caption },
|
||||
...(caption && { caption }),
|
||||
disable_web_page_preview: true,
|
||||
reply_to_message_id: getRepliedToId(ctx.message),
|
||||
};
|
||||
|
||||
return ctx[typeToMethod(type)](content, options)
|
||||
.then(({ message_id }) =>
|
||||
scheduleDeletion(
|
||||
autoDelete(command) && deleteCustom.after)({
|
||||
chat: ctx.chat, message_id
|
||||
}));
|
||||
return ctx[typeToMethod(type)](content, options).then(({ message_id }) =>
|
||||
scheduleDeletion(autoDelete(command) && deleteCustom.after)({
|
||||
chat: ctx.chat,
|
||||
message_id,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = hears(/^! ?(\w+)/, runCustomCmdHandler);
|
||||
|
@ -1,11 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
/** @param { import('../typings/context').ExtendedContext } ctx */
|
||||
const unmatchedHandler = async ctx => {
|
||||
const unmatchedHandler = async (ctx) => {
|
||||
ctx.state[unmatchedHandler.unmatched] = true;
|
||||
if (ctx.chat && ctx.chat.type === 'private') {
|
||||
await ctx.reply(
|
||||
'Sorry, I couldn\'t understand that, do you need /help?',
|
||||
"Sorry, I couldn't understand that, do you need /help?"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
2
index.js
2
index.js
@ -18,7 +18,7 @@ bot.use(
|
||||
require('./plugins'),
|
||||
require('./handlers/commands'),
|
||||
require('./handlers/regex'),
|
||||
require('./handlers/unmatched'),
|
||||
require('./handlers/unmatched')
|
||||
);
|
||||
|
||||
bot.catch(logError);
|
||||
|
7836
package-lock.json
generated
7836
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@ -6,8 +6,10 @@
|
||||
"scripts": {
|
||||
"postversion": "git push --atomic --follow-tags origin develop develop:master",
|
||||
"start": "node index",
|
||||
"lint": "eslint --ext .ts --ext .js .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"lint": "eslint --ext .ts --ext .js . --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"fmt": "prettier --write .",
|
||||
"check": "npm run -s typecheck && npm run -s lint && npm run -s fmt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -37,29 +39,33 @@
|
||||
"jspack": "0.0.4",
|
||||
"millisecond": "^0.1.2",
|
||||
"nedb-promise": "^2.0.1",
|
||||
"ramda": "^0.25.0",
|
||||
"ramda": "^0.28.0",
|
||||
"require-directory": "^2.1.1",
|
||||
"spamwatch": "^0.4.0",
|
||||
"string-replace-async": "^2.0.0",
|
||||
"telegraf": "^4.8.3",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "^4.6.3",
|
||||
"undici": "^4.16.0",
|
||||
"xregexp": "^5.1.0"
|
||||
"telegraf": "^4.12.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5",
|
||||
"undici": "^5.20.0",
|
||||
"xregexp": "^5.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.13.2",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/ramda": "^0.25.51",
|
||||
"@types/ramda": "^0.28.23",
|
||||
"@types/xregexp": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"prettier": "2.0.5"
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||
"@typescript-eslint/parser": "^5.54.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "2.8.4"
|
||||
},
|
||||
"prettier": {
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
# Plugins #
|
||||
# Plugins
|
||||
|
||||
Plugins let you extend the bot with new functionality
|
||||
without touching source code of the bot.
|
||||
|
||||
|
||||
## Using plugins ##
|
||||
## Using plugins
|
||||
|
||||
To use a plugin, put it in this directory and add it's name
|
||||
to `plugins` array in `config.js`.
|
||||
@ -13,8 +12,7 @@ If `plugins` is undefined, no plugins are loaded.
|
||||
However, this behavior may change, don't rely on it.
|
||||
If you want no plugins to be loaded, explicitly set it to empty array.
|
||||
|
||||
|
||||
## Creating a plugin ##
|
||||
## Creating a plugin
|
||||
|
||||
Plugin is basically "requirable"
|
||||
(JS file, or directory with `index.js`)
|
||||
@ -23,13 +21,11 @@ which exports a valid Telegraf handler
|
||||
|
||||
Plugins are similar to [micro-bot] bots.
|
||||
|
||||
## Known plugins
|
||||
|
||||
## Known plugins ##
|
||||
|
||||
* [Captcha](https://gist.github.com/poeti8/d84dfc4538510366a2d89294ff52b4ae): Adds a simple captcha to the bot to kick spam bots on join.
|
||||
* [Banfiles](https://gist.github.com/poeti8/133796200d66049c9bd58e6265a52f68) Ban users that send files with specified extentions.
|
||||
* [Anti Arabic](https://gist.github.com/poeti8/966ccef35d61ad2735dc0120ce3e8760) Bans users that send an Arabic/Persian message.
|
||||
* [Anti X-POST](https://gist.github.com/poeti8/c3057f973466676ca8dbbb1183cd0624) Removes same messages sent by user across one or multiple groups.
|
||||
|
||||
- [Captcha](https://gist.github.com/poeti8/d84dfc4538510366a2d89294ff52b4ae): Adds a simple captcha to the bot to kick spam bots on join.
|
||||
- [Banfiles](https://gist.github.com/poeti8/133796200d66049c9bd58e6265a52f68) Ban users that send files with specified extentions.
|
||||
- [Anti Arabic](https://gist.github.com/poeti8/966ccef35d61ad2735dc0120ce3e8760) Bans users that send an Arabic/Persian message.
|
||||
- [Anti X-POST](https://gist.github.com/poeti8/c3057f973466676ca8dbbb1183cd0624) Removes same messages sent by user across one or multiple groups.
|
||||
|
||||
[micro-bot]: https://github.com/telegraf/micro-bot
|
||||
|
@ -1,10 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const { Telegraf: { compose } } = require('telegraf');
|
||||
const {
|
||||
Telegraf: { compose },
|
||||
} = require('telegraf');
|
||||
|
||||
const { config } = require('../utils/config');
|
||||
const names = config.plugins || [];
|
||||
|
||||
const plugins = names.map(name => `./${name}`).map(require);
|
||||
const plugins = names.map((name) => `./${name}`).map(require);
|
||||
|
||||
module.exports = compose(plugins);
|
||||
|
@ -12,7 +12,7 @@ Command.ensureIndex({
|
||||
unique: true,
|
||||
});
|
||||
|
||||
const addCommand = command =>
|
||||
const addCommand = (command) =>
|
||||
Command.update(
|
||||
{ name: command.name },
|
||||
{ $set: { isActive: false, ...command } },
|
||||
@ -22,7 +22,7 @@ const addCommand = command =>
|
||||
const updateCommand = (data) =>
|
||||
Command.update({ id: data.id, isActive: false }, { $set: data });
|
||||
|
||||
const removeCommand = command => Command.remove(command);
|
||||
const removeCommand = (command) => Command.remove(command);
|
||||
|
||||
const getCommand = (data) => Command.findOne(data);
|
||||
|
||||
|
@ -12,32 +12,26 @@ Group.ensureIndex({
|
||||
unique: true,
|
||||
});
|
||||
|
||||
const addGroup = group =>
|
||||
const addGroup = (group) =>
|
||||
Group.update({ id: group.id }, group, { upsert: true });
|
||||
|
||||
const hideGroup = ({ id }) =>
|
||||
Group.update({ id }, { $set: { link: '' } });
|
||||
const hideGroup = ({ id }) => Group.update({ id }, { $set: { link: '' } });
|
||||
|
||||
const updateGroup = group =>
|
||||
Group.update({ id: group.id }, { $set: group });
|
||||
const updateGroup = (group) => Group.update({ id: group.id }, { $set: group });
|
||||
|
||||
const listGroups = (query = {}) =>
|
||||
Group.find(query);
|
||||
const listGroups = (query = {}) => Group.find(query);
|
||||
|
||||
const listVisibleGroups = () =>
|
||||
Group.find({ $not: { link: '' } });
|
||||
const listVisibleGroups = () => Group.find({ $not: { link: '' } });
|
||||
|
||||
const managesGroup = group =>
|
||||
Group.findOne(group);
|
||||
const managesGroup = (group) => Group.findOne(group);
|
||||
|
||||
const migrateGroup = (oldId, newId) =>
|
||||
Group.update(
|
||||
{ id: oldId, type: 'group' },
|
||||
{ $set: { id: newId, type: 'supergroup' } },
|
||||
{ $set: { id: newId, type: 'supergroup' } }
|
||||
);
|
||||
|
||||
const removeGroup = ({ id }) =>
|
||||
Group.remove({ id });
|
||||
const removeGroup = ({ id }) => Group.remove({ id });
|
||||
|
||||
module.exports = {
|
||||
addGroup,
|
||||
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @typedef { { id: number } | { username: string } } UserQuery
|
||||
* @exports UserQuery
|
||||
*/
|
||||
*/
|
||||
|
||||
// Utils
|
||||
const { strip } = require('../utils/cmd');
|
||||
@ -31,27 +31,25 @@ User.ensureIndex({
|
||||
User.update(
|
||||
{ username: '' },
|
||||
{ $unset: { username: true } },
|
||||
{ multi: true },
|
||||
).then(() =>
|
||||
User.ensureIndex({ fieldName: 'username', sparse: true }));
|
||||
{ multi: true }
|
||||
).then(() => User.ensureIndex({ fieldName: 'username', sparse: true }));
|
||||
|
||||
const normalizeTgUser = R.pipe(
|
||||
R.pick([ 'first_name', 'id', 'last_name', 'username' ]),
|
||||
R.pick(['first_name', 'id', 'last_name', 'username']),
|
||||
R.evolve({ username: R.toLower }),
|
||||
R.merge({ first_name: '', last_name: '' }),
|
||||
R.mergeDeepRight({ first_name: '', last_name: '' })
|
||||
);
|
||||
|
||||
const getUpdatedDocument = R.prop(1);
|
||||
|
||||
const getUser = user =>
|
||||
User.findOne(user);
|
||||
const getUser = (user) => User.findOne(user);
|
||||
|
||||
const updateUser = async (rawTgUser) => {
|
||||
const tgUser = normalizeTgUser(rawTgUser);
|
||||
|
||||
const { id, username } = tgUser;
|
||||
|
||||
const [ rawDbUser ] = await Promise.all([
|
||||
const [rawDbUser] = await Promise.all([
|
||||
getUser({ id }),
|
||||
User.update({ $not: { id }, username }, { $unset: { username: true } }),
|
||||
]);
|
||||
@ -60,7 +58,7 @@ const updateUser = async (rawTgUser) => {
|
||||
return User.update(
|
||||
{ id },
|
||||
{ status: 'member', warns: [], ...tgUser },
|
||||
{ returnUpdatedDocs: true, upsert: true },
|
||||
{ returnUpdatedDocs: true, upsert: true }
|
||||
).then(getUpdatedDocument);
|
||||
}
|
||||
|
||||
@ -70,7 +68,7 @@ const updateUser = async (rawTgUser) => {
|
||||
return User.update(
|
||||
{ id },
|
||||
{ $set: tgUser },
|
||||
{ returnUpdatedDocs: true },
|
||||
{ returnUpdatedDocs: true }
|
||||
).then(getUpdatedDocument);
|
||||
}
|
||||
|
||||
@ -78,16 +76,11 @@ const updateUser = async (rawTgUser) => {
|
||||
};
|
||||
|
||||
const admin = ({ id }) =>
|
||||
User.update(
|
||||
{ id },
|
||||
{ $set: { status: 'admin', warns: [] } },
|
||||
);
|
||||
User.update({ id }, { $set: { status: 'admin', warns: [] } });
|
||||
|
||||
const getAdmins = () =>
|
||||
User.find({ status: 'admin' });
|
||||
const getAdmins = () => User.find({ status: 'admin' });
|
||||
|
||||
const unadmin = ({ id }) =>
|
||||
User.update({ id }, { $set: { status: 'member' } });
|
||||
const unadmin = ({ id }) => User.update({ id }, { $set: { status: 'member' } });
|
||||
|
||||
const isAdmin = (user) => {
|
||||
if (!user) return false;
|
||||
@ -101,14 +94,14 @@ const ban = ({ id }, ban_details) =>
|
||||
User.update(
|
||||
{ id, $not: { status: 'admin' } },
|
||||
{ $set: { ban_details, status: 'banned' } },
|
||||
{ upsert: true },
|
||||
{ upsert: true }
|
||||
);
|
||||
|
||||
const batchBan = (users, ban_details) =>
|
||||
User.update(
|
||||
{ $or: users.map(strip), $not: { status: 'admin' } },
|
||||
{ $set: { ban_details, status: 'banned' } },
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
{ multi: true, returnUpdatedDocs: true }
|
||||
).then(getUpdatedDocument);
|
||||
|
||||
const ensureExists = ({ id }) =>
|
||||
@ -120,7 +113,7 @@ const unban = ({ id }) =>
|
||||
{
|
||||
$set: { status: 'member' },
|
||||
$unset: { ban_details: true, ban_reason: true },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
@ -130,7 +123,7 @@ const permit = (user, { by_id, date }) =>
|
||||
User.update(
|
||||
user,
|
||||
{ $set: { permit: { by_id, date } } },
|
||||
{ returnUpdatedDocs: true },
|
||||
{ returnUpdatedDocs: true }
|
||||
).then(getUpdatedDocument);
|
||||
|
||||
/**
|
||||
@ -140,7 +133,7 @@ permit.revoke = (user) =>
|
||||
User.update(
|
||||
{ permit: { $exists: true }, ...strip(user) },
|
||||
{ $unset: { permit: true } },
|
||||
{ returnUpdatedDocs: true },
|
||||
{ returnUpdatedDocs: true }
|
||||
).then(getUpdatedDocument);
|
||||
|
||||
permit.isValid = (p) => Date.now() - ms('24h') < p?.date;
|
||||
@ -153,7 +146,7 @@ const warn = ({ id }, reason, { amend }) =>
|
||||
$push: { warns: reason },
|
||||
$unset: { permit: true },
|
||||
},
|
||||
{ returnUpdatedDocs: true },
|
||||
{ returnUpdatedDocs: true }
|
||||
).then(getUpdatedDocument);
|
||||
|
||||
const unwarn = ({ id }, warnQuery) =>
|
||||
@ -163,10 +156,10 @@ const unwarn = ({ id }, warnQuery) =>
|
||||
$pull: { warns: warnQuery },
|
||||
$set: { status: 'member' },
|
||||
$unset: { ban_details: true, ban_reason: true },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const nowarns = query => unwarn(query, {});
|
||||
const nowarns = (query) => unwarn(query, {});
|
||||
|
||||
const verifyCaptcha = ({ id }, captcha = true) =>
|
||||
User.update({ id }, { $set: { captcha } });
|
||||
|
@ -5,6 +5,7 @@
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"strict": true,
|
||||
"target": "ES2019"
|
||||
"target": "ES2019",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
6
typings/config.d.ts
vendored
6
typings/config.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
import type { InlineKeyboardMarkup } from "telegraf/typings/telegram-types";
|
||||
import type { InlineKeyboardMarkup } from 'telegraf/types';
|
||||
|
||||
export type InlineKeyboard = InlineKeyboardMarkup["inline_keyboard"];
|
||||
export type InlineKeyboard = InlineKeyboardMarkup['inline_keyboard'];
|
||||
|
||||
/**
|
||||
* String to be parsed by https://npmjs.com/millisecond,
|
||||
@ -44,7 +44,7 @@ export interface Config {
|
||||
* Which messages with commands should be deleted?
|
||||
* Defaults to 'own' -- don't delete commands meant for other bots.
|
||||
*/
|
||||
deleteCommands?: "all" | "own" | "none";
|
||||
deleteCommands?: 'all' | 'own' | 'none';
|
||||
|
||||
deleteCustom?: {
|
||||
longerThan: number; // UTF-16 characters
|
||||
|
20
typings/context.d.ts
vendored
20
typings/context.d.ts
vendored
@ -1,13 +1,9 @@
|
||||
import type {
|
||||
ExtraReplyMessage,
|
||||
Message,
|
||||
User,
|
||||
} from "telegraf/typings/telegram-types";
|
||||
import type { Context } from "telegraf";
|
||||
import type { TgHtml } from "../utils/html";
|
||||
import type { Convenience, Message, User } from 'telegraf/types';
|
||||
import type { Context } from 'telegraf';
|
||||
import type { TgHtml } from '../utils/html';
|
||||
|
||||
interface DbUser {
|
||||
status: "member" | "admin" | "banned";
|
||||
status: 'member' | 'admin' | 'banned';
|
||||
}
|
||||
|
||||
export interface ContextExtensions {
|
||||
@ -34,24 +30,24 @@ export interface ContextExtensions {
|
||||
amend?: boolean;
|
||||
reason: string;
|
||||
userToWarn: User;
|
||||
mode: "auto" | "manual";
|
||||
mode: 'auto' | 'manual';
|
||||
}
|
||||
): Promise<Message>;
|
||||
|
||||
loggedReply(
|
||||
this: ExtendedContext,
|
||||
html: TgHtml,
|
||||
extra?: ExtraReplyMessage
|
||||
extra?: Convenience.ExtraReplyMessage
|
||||
): Promise<Message>;
|
||||
replyWithHTML(
|
||||
this: void,
|
||||
html: string | TgHtml,
|
||||
extra?: ExtraReplyMessage
|
||||
extra?: Convenience.ExtraReplyMessage
|
||||
): Promise<Message>;
|
||||
replyWithCopy(
|
||||
this: ExtendedContext,
|
||||
content: Message,
|
||||
options?: ExtraReplyMessage
|
||||
options?: Convenience.ExtraReplyMessage
|
||||
): Promise<Message>;
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,25 @@
|
||||
import XRegExp = require("xregexp");
|
||||
import type { Message, MessageEntity } from "telegraf/typings/telegram-types";
|
||||
import * as XRegExp from 'xregexp';
|
||||
import type { Message, MessageEntity } from 'telegraf/types';
|
||||
|
||||
export const strip = ({ id, username }) =>
|
||||
id ? { id } : { username: username.toLowerCase() };
|
||||
|
||||
const toUserObject = (s) =>
|
||||
s.user || (/^\d+$/.test(s) ? { id: +s } : { username: s.replace("@", "") });
|
||||
s.user || (/^\d+$/.test(s) ? { id: +s } : { username: s.replace('@', '') });
|
||||
|
||||
const isTextMention = (m: MessageEntity) => m.type === "text_mention";
|
||||
const isTextMention = (m: MessageEntity) => m.type === 'text_mention';
|
||||
|
||||
const spliceOut = (s: string, { offset, length }: MessageEntity) =>
|
||||
s.slice(0, offset) + s.slice(offset + length);
|
||||
|
||||
const botReply = ({ from, entities = [] }: Message) => {
|
||||
const textMentions = entities.filter(isTextMention);
|
||||
const botReply = (message: Message) => {
|
||||
const { from } = message;
|
||||
const textMentions =
|
||||
'entities' in message && typeof message.entities !== 'undefined'
|
||||
? (message.entities.filter(
|
||||
isTextMention
|
||||
) as MessageEntity.TextMentionMessageEntity[])
|
||||
: [];
|
||||
|
||||
return from?.is_bot && textMentions.length === 1 && [textMentions[0].user];
|
||||
};
|
||||
@ -27,7 +33,7 @@ function* extractFlags(flagS: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const regex = XRegExp.tag("snx")`^
|
||||
const regex = XRegExp.tag('snx')`^
|
||||
\/\w+(@\w+)?
|
||||
(?<flagS> ${flagsRegex}*)
|
||||
(?<ids> (\s+@\w+|\s+\d+)*)
|
||||
@ -36,25 +42,29 @@ const regex = XRegExp.tag("snx")`^
|
||||
$`;
|
||||
|
||||
export const isCommand = (
|
||||
message?: Message
|
||||
): message is Message & {
|
||||
message?: Message.TextMessage
|
||||
): message is Message.TextMessage & {
|
||||
text: string;
|
||||
entities: [{ type: "bot_command"; offset: 0 }];
|
||||
entities: [{ type: 'bot_command'; offset: 0 }];
|
||||
} => {
|
||||
const firstEntity = message?.entities?.[0];
|
||||
return firstEntity?.type === "bot_command" && firstEntity.offset === 0;
|
||||
return firstEntity?.type === 'bot_command' && firstEntity.offset === 0;
|
||||
};
|
||||
|
||||
export const parse = (message?: Message) => {
|
||||
export const parse = (message?: Message.TextMessage) => {
|
||||
if (!isCommand(message)) {
|
||||
throw new TypeError("Not a command");
|
||||
throw new TypeError('Not a command');
|
||||
}
|
||||
const textMentions = message.entities.filter(isTextMention);
|
||||
const noTextMentions = textMentions.reduceRight(spliceOut, message.text);
|
||||
|
||||
const { flagS, ids, reason = "" } = XRegExp.exec(noTextMentions, regex)?.groups!;
|
||||
const {
|
||||
flagS,
|
||||
ids,
|
||||
reason = '',
|
||||
} = XRegExp.exec(noTextMentions, regex)?.groups!;
|
||||
const flags = new Map(extractFlags(flagS));
|
||||
const users = textMentions.concat(ids.match(/@\w+|\d+/g) || []);
|
||||
const users = [...textMentions, ...(ids.match(/@\w+|\d+/g) || [])];
|
||||
const { reply_to_message } = message;
|
||||
|
||||
// prettier-ignore
|
||||
|
@ -1,24 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const camelToSnake = s => s.replace(/[A-Z]/g, c => '_' + c.toLowerCase());
|
||||
const camelToSnake = (s) => s.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
|
||||
|
||||
exports.stringify = ({ command = '', flags = {}, reason = '' }) => {
|
||||
const flagS = Object.entries(flags)
|
||||
.flatMap(([ key, value ]) => {
|
||||
.flatMap(([key, value]) => {
|
||||
switch (value) {
|
||||
case null:
|
||||
case false:
|
||||
case undefined: // eslint-disable-line no-undefined
|
||||
return [];
|
||||
case true:
|
||||
return [ '-' + camelToSnake(key) ];
|
||||
default:
|
||||
return [ `-${camelToSnake(key)}=${value}` ];
|
||||
case null:
|
||||
case false:
|
||||
case undefined: // eslint-disable-line no-undefined
|
||||
return [];
|
||||
case true:
|
||||
return ['-' + camelToSnake(key)];
|
||||
default:
|
||||
return [`-${camelToSnake(key)}=${value}`];
|
||||
}
|
||||
}).join(' ');
|
||||
return [
|
||||
'/' + command,
|
||||
flagS,
|
||||
reason
|
||||
].filter(Boolean).join(' ');
|
||||
})
|
||||
.join(' ');
|
||||
return ['/' + command, flagS, reason].filter(Boolean).join(' ');
|
||||
};
|
||||
|
@ -1,15 +1,15 @@
|
||||
import replace = require("string-replace-async");
|
||||
import { getCommand } from "../../stores/command";
|
||||
import replace = require('string-replace-async');
|
||||
import { getCommand } from '../../stores/command';
|
||||
|
||||
export const substom = (reason: string): Promise<string> =>
|
||||
replace(reason, /!\s?(\w+)\s*|.+/g, async (match, name) => {
|
||||
if (!name) return match;
|
||||
const command = await getCommand({
|
||||
name: name.toLowerCase(),
|
||||
role: { $ne: "master" },
|
||||
type: "copy",
|
||||
role: { $ne: 'master' },
|
||||
type: 'copy',
|
||||
});
|
||||
const text = command?.content.text || command?.content.caption;
|
||||
if (!text) return match;
|
||||
return text + " ";
|
||||
return text + ' ';
|
||||
});
|
||||
|
@ -10,26 +10,30 @@ const ms = require('millisecond');
|
||||
|
||||
const { expireWarnsAfter = Infinity } = config;
|
||||
|
||||
const isNewerThan = date => warning => warning.date >= date;
|
||||
const isNewerThan = (date) => (warning) => warning.date >= date;
|
||||
|
||||
/** @param {Date} date */
|
||||
const isWarnNotExpired = date =>
|
||||
const isWarnNotExpired = (date) =>
|
||||
isNewerThan(date.getTime() - ms(expireWarnsAfter));
|
||||
|
||||
const stringOrNumber = x => [ 'string', 'number' ].includes(typeof x);
|
||||
const stringOrNumber = (x) => ['string', 'number'].includes(typeof x);
|
||||
|
||||
// @ts-ignore
|
||||
const masters = [].concat(config.master);
|
||||
|
||||
if (!masters.every(x => stringOrNumber(x) && /^@?\w+$/.test(x))) {
|
||||
throw new Error('Invalid value for `master` in config file: ' +
|
||||
config.master);
|
||||
if (!masters.every((x) => stringOrNumber(x) && /^@?\w+$/.test(x))) {
|
||||
throw new Error(
|
||||
'Invalid value for `master` in config file: ' + config.master
|
||||
);
|
||||
}
|
||||
|
||||
const isMaster = user =>
|
||||
user && masters.some(x =>
|
||||
user.id === Number(x) ||
|
||||
user.username && eq.username(user.username, String(x)));
|
||||
const isMaster = (user) =>
|
||||
user &&
|
||||
masters.some(
|
||||
(x) =>
|
||||
user.id === Number(x) ||
|
||||
(user.username && eq.username(user.username, String(x)))
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
config,
|
||||
|
@ -1,24 +1,25 @@
|
||||
'use strict';
|
||||
// from https://gist.github.com/f9e184b78bbfc4419bc1ee70c238ca6f
|
||||
|
||||
import dedent = require("dedent-js");
|
||||
import dedent = require('dedent-js');
|
||||
|
||||
const symbol = Symbol("TgHtml.symbol");
|
||||
const symbol = Symbol('TgHtml.symbol');
|
||||
|
||||
type Escapable = bigint | boolean | Error | number | string;
|
||||
type Sub = Escapable | TgHtml;
|
||||
|
||||
const escapeHtml = (s: Escapable) =>
|
||||
String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<");
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<');
|
||||
|
||||
const isTgHtml = (o: Sub): o is TgHtml => typeof o?.[symbol] === "string";
|
||||
const isTgHtml = (o: Sub): o is TgHtml => typeof o?.[symbol] === 'string';
|
||||
const toHtml = (o: Sub) => (isTgHtml(o) ? o[symbol] : escapeHtml(o));
|
||||
|
||||
export class TgHtml {
|
||||
readonly [Symbol.toStringTag] = "TgHtml";
|
||||
readonly [Symbol.toStringTag] = 'TgHtml';
|
||||
private readonly [symbol]: string;
|
||||
private constructor(s: string) {
|
||||
this[symbol] = s;
|
||||
@ -37,7 +38,7 @@ export class TgHtml {
|
||||
return new TgHtml(s.map(toHtml).join(toHtml(sep)));
|
||||
}
|
||||
static concat(...s: Sub[]) {
|
||||
return TgHtml.join("", s);
|
||||
return TgHtml.join('', s);
|
||||
}
|
||||
static pre(s: Sub) {
|
||||
return TgHtml.tag`<pre>${s}</pre>`;
|
||||
@ -47,4 +48,4 @@ export class TgHtml {
|
||||
export const html = (raw: TemplateStringsArray, ...subs: Sub[]) =>
|
||||
TgHtml.tag(raw, ...subs);
|
||||
|
||||
export const lrm = "\u200E";
|
||||
export const lrm = '\u200E';
|
||||
|
@ -5,12 +5,12 @@
|
||||
|
||||
const { inspect } = require('util');
|
||||
|
||||
const logError = err => console.error(err);
|
||||
const logError = (err) => console.error(err);
|
||||
|
||||
const print = value =>
|
||||
const print = (value) =>
|
||||
console.log(inspect(value, { colors: true, depth: null }));
|
||||
|
||||
module.exports = {
|
||||
logError,
|
||||
print
|
||||
print,
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const username = s => s.replace(/^@/, '').toLowerCase();
|
||||
const username = (s) => s.replace(/^@/, '').toLowerCase();
|
||||
|
||||
module.exports = {
|
||||
username,
|
||||
|
@ -13,4 +13,4 @@ exports.shouldKick = (function () {
|
||||
|
||||
const client = new Client(config.spamwatch.token, config.spamwatch.host);
|
||||
return ({ id }) => client.getBan(id);
|
||||
}());
|
||||
})();
|
||||
|
42
utils/tg.js
42
utils/tg.js
@ -7,31 +7,31 @@ const { telegram } = require('../bot');
|
||||
const { html, lrm } = require('./html');
|
||||
const R = require('ramda');
|
||||
|
||||
const replyId = R.path([ 'reply_to_message', 'message_id' ]);
|
||||
const replyId = R.path(['reply_to_message', 'message_id']);
|
||||
|
||||
const { isCommand } = require('../utils/cmd');
|
||||
|
||||
const inlineKeyboard = (...inline_keyboard) =>
|
||||
({ reply_markup: { inline_keyboard } });
|
||||
const inlineKeyboard = (...inline_keyboard) => ({
|
||||
reply_markup: { inline_keyboard },
|
||||
});
|
||||
|
||||
const msgLink = msg =>
|
||||
const msgLink = (msg) =>
|
||||
`https://t.me/c/${msg.chat.id.toString().slice(4)}/${msg.message_id}`;
|
||||
|
||||
const link = ({ id, first_name }) =>
|
||||
html`${lrm}<a href="tg://user?id=${id}">${first_name}</a> [<code>${id}</code>]`;
|
||||
html`${lrm}<a href="tg://user?id=${id}">${first_name}</a>
|
||||
[<code>${id}</code>]`;
|
||||
|
||||
const quietLink = (user) =>
|
||||
user.username
|
||||
? html`<a href="t.me/${user.username}">${user.first_name}</a>`
|
||||
: html`<a href="tg://user?id=${user.id}">${user.first_name}</a>`;
|
||||
|
||||
const displayUser = user =>
|
||||
user.first_name
|
||||
? link(user)
|
||||
: html`[<code>${user.id}</code>]`;
|
||||
const displayUser = (user) =>
|
||||
user.first_name ? link(user) : html`[<code>${user.id}</code>]`;
|
||||
|
||||
/** @param {number | string | false} ms */
|
||||
const deleteAfter = ms => (ctx, next) => {
|
||||
const deleteAfter = (ms) => (ctx, next) => {
|
||||
if (ms !== false) {
|
||||
setTimeout(ctx.deleteMessage.bind(ctx), millisecond(ms));
|
||||
}
|
||||
@ -39,18 +39,20 @@ const deleteAfter = ms => (ctx, next) => {
|
||||
};
|
||||
|
||||
/** @param {number | string | false} ms */
|
||||
const scheduleDeletion = (ms = 5 * 60 * 1000) => message => {
|
||||
const { chat, message_id } = message;
|
||||
const scheduleDeletion =
|
||||
(ms = 5 * 60 * 1000) =>
|
||||
(message) => {
|
||||
const { chat, message_id } = message;
|
||||
|
||||
if (chat.type !== 'private' && ms !== false) {
|
||||
message.timeout = setTimeout(
|
||||
() => telegram.deleteMessage(chat.id, message_id),
|
||||
millisecond(ms),
|
||||
);
|
||||
}
|
||||
if (chat.type !== 'private' && ms !== false) {
|
||||
message.timeout = setTimeout(
|
||||
() => telegram.deleteMessage(chat.id, message_id),
|
||||
millisecond(ms)
|
||||
);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
return message;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
deleteAfter,
|
||||
|
Loading…
x
Reference in New Issue
Block a user