From 8fcfea6bb990ea35d6422f9f54f39ca2bccaf0c4 Mon Sep 17 00:00:00 2001 From: Wojciech Pawlik Date: Mon, 29 Jun 2020 15:07:12 +0200 Subject: [PATCH] Add /permit, closes #72 --- README.md | 1 + handlers/commands/commands.js | 1 + handlers/commands/permit.ts | 26 ++++++++++++++ handlers/commands/user.js | 14 ++++++-- handlers/middlewares/checkLinks.ts | 10 +++++- handlers/middlewares/removeChannelForwards.js | 12 +++++-- stores/user.js | 35 ++++++++++++++++++- 7 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 handlers/commands/permit.ts diff --git a/README.md b/README.md index 3746f40..714f153 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Command | Role | Available at | Description `/warn ` | _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 ` | _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. diff --git a/handlers/commands/commands.js b/handlers/commands/commands.js index 76418a9..fece333 100644 --- a/handlers/commands/commands.js +++ b/handlers/commands/commands.js @@ -25,6 +25,7 @@ const adminCommands = `\ /warn <reason> - Warns the user. /unwarn - Removes the last warn from the user. /nowarns - Clears warns for the user. +/permit - Permits the user to advertise once, within 24 hours. /ban <reason> - Bans the user from groups. /unban - Removes the user from ban list. /user - Shows user's status and warns. diff --git a/handlers/commands/permit.ts b/handlers/commands/permit.ts new file mode 100644 index 0000000..31c2772 --- /dev/null +++ b/handlers/commands/permit.ts @@ -0,0 +1,26 @@ +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; + + const { targets } = parse(ctx.message); + if (targets.length !== 1) { + return ctx + .replyWithHTML("â„šī¸ Specify one user to permit.") + .then(scheduleDeletion()); + } + + const permitted = await permit(strip(targets[0]), { + by_id: ctx.from.id, + date: new Date(), + }); + + return ctx.replyWithHTML(html` + 🎟 ${lrm}${ctx.from.first_name} permitted ${displayUser(permitted)} to + promote once within the next 24 hours! + `); +}; diff --git a/handlers/commands/user.js b/handlers/commands/user.js index 517b35e..9bb6ca8 100644 --- a/handlers/commands/user.js +++ b/handlers/commands/user.js @@ -9,7 +9,7 @@ const { isMaster, isWarnNotExpired } = require('../../utils/config'); const { parse, strip } = require('../../utils/cmd'); // DB -const { getUser } = require('../../stores/user'); +const { getUser, permit } = require('../../stores/user'); /** @param {Date} date */ const formatDate = date => @@ -100,9 +100,19 @@ const getWarnsHandler = async ({ from, message, replyWithHTML }) => { formatDate(theUser.createdAt), ); - return replyWithHTML(TgHtml.join('\n\n', [ + 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)}` + : ''; + + const oneliners = TgHtml.join('\n', [ header, firstSeen, + permitS, + ].filter(isNotEmpty)); + + return replyWithHTML(TgHtml.join('\n\n', [ + oneliners, userWarns, banReason, ].filter(isNotEmpty))).then(scheduleDeletion()); diff --git a/handlers/middlewares/checkLinks.ts b/handlers/middlewares/checkLinks.ts index 739a610..3457281 100644 --- a/handlers/middlewares/checkLinks.ts +++ b/handlers/middlewares/checkLinks.ts @@ -1,10 +1,11 @@ /* 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 fetch from "node-fetch"; -import { isAdmin } from "../../stores/user"; import { jspack } from "jspack"; import { managesGroup } from "../../stores/group"; import type { MessageEntity } from "telegraf/typings/telegram-types"; @@ -223,6 +224,13 @@ export = async (ctx: ExtendedContext, next) => { if (userToWarn.id === 777000) return next(); if (await isAdmin(userToWarn)) return next(); + if (await permit.revoke(userToWarn)) { + await ctx.replyWithHTML(html` + ${lrm}${userToWarn.first_name} used 🎟 permit! + `); + return next(); + } + ctx.deleteMessage().catch(() => null); return ctx.warn({ admin: ctx.botInfo!, diff --git a/handlers/middlewares/removeChannelForwards.js b/handlers/middlewares/removeChannelForwards.js index ebac4ec..75e2e2c 100644 --- a/handlers/middlewares/removeChannelForwards.js +++ b/handlers/middlewares/removeChannelForwards.js @@ -3,11 +3,12 @@ const R = require('ramda'); const { optional, passThru } = require('telegraf'); +const { permit } = require('../../stores/user'); + +const { html, lrm } = require('../../utils/html'); const { excludeLinks = [] } = require('../../utils/config').config; if (excludeLinks === false || excludeLinks === '*') { - - /** @type { import('../../typings/context').GuardMiddlewareFn } */ module.exports = passThru(); return; } @@ -46,7 +47,12 @@ const pred = R.allPass([ ]); /** @param { import('../../typings/context').ExtendedContext } ctx */ -const handler = ctx => { +const handler = async (ctx, next) => { + if (await permit.revoke(ctx.from)) { + await ctx.replyWithHTML(html`${lrm}${ctx.from.first_name} used 🎟 permit!`); + return next(); + } + ctx.deleteMessage().catch(() => null); return ctx.warn({ admin: ctx.botInfo, diff --git a/stores/user.js b/stores/user.js index 6fa25ef..b4fc50e 100644 --- a/stores/user.js +++ b/stores/user.js @@ -1,9 +1,15 @@ 'use strict'; +/** + * @typedef { { id: number } | { username: string } } UserQuery + * @exports UserQuery +*/ + // Utils const { strip } = require('../utils/cmd'); const Datastore = require('nedb-promise'); +const ms = require('millisecond'); const R = require('ramda'); const User = new Datastore({ @@ -117,10 +123,36 @@ const unban = ({ id }) => }, ); +/** + * @param {UserQuery} user + */ +const permit = (user, { by_id, date }) => + User.update( + user, + { $set: { permit: { by_id, date } } }, + { returnUpdatedDocs: true }, + ).then(getUpdatedDocument); + +/** + * @param {UserQuery} user + */ +permit.revoke = (user) => + User.update( + { permit: { $exists: true }, ...strip(user) }, + { $unset: { permit: true } }, + { returnUpdatedDocs: true }, + ).then(getUpdatedDocument); + +permit.isValid = (p) => Date.now() - ms('24h') < p?.date; + const warn = ({ id }, reason, { amend }) => User.update( { id, $not: { status: 'admin' } }, - { $pop: { warns: +!!amend }, $push: { warns: reason } }, + { + $pop: { warns: +!!amend }, + $push: { warns: reason }, + $unset: { permit: true }, + }, { returnUpdatedDocs: true }, ).then(getUpdatedDocument); @@ -145,6 +177,7 @@ module.exports = { getUser, isAdmin, nowarns, + permit, unadmin, unban, unwarn,