Repository: axiref/telegram-pwsbot
Branch: master
Commit: 0594f6bcf2e0
Files: 23
Total size: 52.0 KB
Directory structure:
gitextract_gh6f860p/
├── .babelrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── ecosystem.config.js
├── env.example
├── package.json
├── readme.md
└── src/
├── core.js
├── handler/
│ ├── botCommand.js
│ ├── commandHandler.js
│ ├── msgControl.js
│ ├── msgHandler.js
│ └── queryHandler.js
├── lang/
│ ├── zh-CN.json
│ └── zh-TW.json
├── main.js
├── model/
│ ├── BlackList.js
│ ├── Db.js
│ ├── Message.js
│ └── Re.js
└── utils/
├── Lang.js
└── helper.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
["env", {
"targets": {
"node": "current"
}
}],
"es2015",
"stage-2"
],
"plugins": ["transform-runtime"]
}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# MacOS
.DS_Store
# Database
db.json
# Dependency directories
node_modules/
# Production
dist/
# dotenv environment variables file
.env
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- '12.16.1'
- '12.1.0'
before_install:
- echo -e "Token=$BOT_TOKEN\nAdmin=$BOT_ADMIN\nAutoMute=\nLang=zh_TW\nChannel=$BOT_CHANNEL" > .env
script:
- npm run test
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2019 wepy
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: ecosystem.config.js
================================================
module.exports = {
apps : [{
name: 'pwsbot',
script: 'npm run start',
// Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
}
}],
};
================================================
FILE: env.example
================================================
# 机器人Token,必填,透过@Botfather机器人之父获取
Token=
# 管理员的userid(数字),必填(用于验证配置审稿群的权限),可透过@userinfo 机器人得到
Admin=
# 投稿到的频道,必填,例 @abcdefg
Channel=
# 夜间消音,留空则禁用。填写时区则夜间00:00~6:50AM自动静音推送 (时区列表 http://php.net/manual/zh/timezones.php)
AutoMute=Asia/Taipei
# 审稿群ID(无需手动填写,将机器人加到审稿群后,透过 /setgroup 命令自动设定)
Group=
# 机器人ID (无需手动填写,自动获取)
BotID=
# 机器人用户名 (无需手动填写,自动获取)
BotUserName=
# 语言 (不带后缀的lang目录下的文件)
Lang=zh-CN
# !!! 必须将这个文件拷贝一份后并重命名为 .env 并修改上方配置才有效 !!! #
================================================
FILE: package.json
================================================
{
"name": "pwsbot",
"version": "1.0.0",
"description": "一个投稿机器人telegram",
"scripts": {
"start": "npm run build && node dist/main.js",
"dev": "export nodemon --ignore db.json --exec babel-node src/main.js",
"build": "babel src --out-dir dist --source-maps inline && cp -r src/lang dist",
"test": "export BOT_ENV=test && npm run start"
},
"author": "axiref",
"license": "MIT",
"dependencies": {
"dotenv": "^6.2.0",
"lowdb": "^1.0.0",
"node-telegram-bot-api": "^0.30.0",
"update-dotenv": "^1.1.1"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/node": "^7.2.2",
"babel-cli": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"nodemon": "^1.18.9"
}
}
================================================
FILE: readme.md
================================================
# PWS - Telegram投稿机器人
[](https://travis-ci.org/axiref/telegram-pwsbot)

此机器人帮助订阅者向频道投稿,支援多图(MediaGroup)投稿、稿件附加评论、夜间自动消音推送、多语言、黑名单等功能。
# 安装
先克隆项目
```bash
git clone https://github.com/axiref/telegram-pwsbot
cd telegram-pwsbot
npm install
```
然后创建配置文件,直接复制一份 `env.example`
```bash
cp env.example .env
vim .env
```
请务必填写下列参数:
- `Token`,机器人的令牌,可透过 [@botfather](https://t.me/botfather) 获取
- `Admin`,管理员,可透过 [@userinfobot](https://t.me/userinfobot) 获取,将**数字**ID复制填写
- `Channel`,要投稿到的频道,填写格式为`@频道ID`,如`@ruanyuww`
另外你可以设置下列可选参数:
- `AutoMute`,夜间静音推送(00:00AM~7:00AM),留空则禁用,需填写时区 ([时区列表](http://php.net/manual/zh/timezones.php))
- `Lang`,语言,可选 `zh-CN`、`zh-TW`,分别为中文简体🇨🇳,中文正体🇹🇼
你可以修改和添加自己的语言,在 `/src/lang`目录下,`<语言名>.json`命名的文件
> 注意:每次修改.env配置文件,都需要重新启动项目
填写完毕后,使用
```bash
npm run start
```
即可运行项目,此时你可以观察终端有没有报错
若一切正常,但也请不要就这样运行项目,这样并不安全,你的机器人**一定会罢工**
**强烈**推荐使用 [PM2](https://www.npmjs.com/package/pm2) 来守护进程
使用下列命令安装 PM2
```bash
npm install pm2 -g
```
由于此项目目录已经携带了PM2的配置文件,你只需要在项目目录下运行
```bash
pm2 start
```
就可以看到名为 `pwsbot`的任务已经被运行,重启项目可使用 `pm2 restart pwsbot` 命令来完成。
项目启动后,必须将你的机器人添加到审稿群,机器人会将收到的稿件转发至审稿群,群内所有人皆可审核稿件,如果你没有审稿群,你应该建立一个。
将机器人加到审稿群后,**务必在审稿群使用** `/setgroup` 命令来初始化机器人 (此命令可设置当前群为审稿群),只需运行一次。
然后需要将机器人添加到频道,普通的添加是无法将机器人添加到频道,你应该点开频道管理员列表,新增一个管理员,然后输入机器人的username,透过这样的方式才能将机器人添加到频道。
> 机器人在审稿群可以不是管理员,但在频道需要是管理员。
由此,部署完毕,你的投稿机器人应该已经可以正常工作了 🎉
# 命令
| 命令 | 说明 | 场景 |
| ---------------- | ----------------------------------------------------- | -------- |
| /start | 显示投稿说明 | 仅私聊 |
| /version | 显示机器人版权 | 私聊和群 |
| /setgroup | 设置当前群为审稿群<br />**必须设置审稿群** | 群 |
| /ok <评论> | 通过一个稿件,也可附加评论<br />也可直接`/ok`通过稿件 | 群 |
| /no <理由> | 拒绝一个稿件,需附加理由 | 群 |
| /re <回复内容> | 回复一个稿件,需回复内容 | 群 |
| /ban <用户ID> | 回复稿件或者输入ID可拉黑一个用户 | 群 |
| /unban <用户ID> | 回复稿件或者输入ID可解除拉黑一个用户 | 群 |
| /unre <用户ID> | 结束用户的对话状态 | 群 |
| /pwshelp | 显示更多可对稿件采取的命令 | 群 |
| /echo <回复内容> | 回复给用户一次,但不进入对话模式 | 群 |
# License
MIT
================================================
FILE: src/core.js
================================================
import TeleBot from 'node-telegram-bot-api';
import helper from './utils/helper';
import Lang from './utils/Lang';
import blacklist from './model/BlackList';
import Message from './model/Message';
import re from './model/Re';
const subs = new Message('subs');// 稿件
let config = require('dotenv').config().parsed;
if (config.AutoMute) {
process.env.TZ = config.AutoMute; //切换时区
}
if (!config) {
throw new Error("不存在 .env 配置,请确保将项目目录下具有 .env 配置文件!(配置文件模板:env.example)");
} else if (!config.Token) {
throw new Error(".env 配置文件中不存在Token,请确保正确填写!")
} else if (!config.Admin) {
throw new Error(".env 配置文件中不存在Admin,请确保正确填写!")
} else if (!config.Channel) {
throw new Error(".env 配置文件中不存在Channel,请确保正确填写!")
}
const bot = new TeleBot(config.Token, {polling: true});
// 保存机器人ID和UserName
bot.getMe().then(info => { helper.updateConfig({BotID: info.id, BotUserName: info.username }) })
/**
* callback query data
* @type {Object}
*/
const vars = {
REC_ANY: 'receive:anonymous',
REC_REAL: 'receive:real',
SUB_ANY: 'submission_type:anonymous',
SUB_REAL: 'submission_type:real',
SUB_CANCEL: 'cancel:submission',
BOT_NOAUTH_KICK: 'ETELEGRAM: 403 Forbidden: bot was kicked from the channel chat',
BOT_NOAUTH: 'ETELEGRAM: 403 Forbidden: bot is not a member of the channel chat',
BOT_BLOCK: 'ETELEGRAM: 403 Forbidden: bot was blocked by the user',
}
// 语言工具
const lang = new Lang(config.Lang, vars);
export {config, subs, bot, vars, helper, lang, blacklist, re};
================================================
FILE: src/handler/botCommand.js
================================================
import {config, bot, helper, lang, subs, blacklist, re} from '../core';
import onText from './commandHandler';
import msgControl from './msgControl';
/**
* 解除对用户的封锁
* @param {[type]} /\/unban (.+)|\/unban/ [description]
* @param {[type]} (msg, match) [description]
* @return {[type]} [description]
*/
onText(/\/unban (.+)|\/unban/, ({ rep, msg, match, repMsg }) => {
if (helper.isPrivate(msg)) { return console.warn('不能运行在Private私聊状态下') }
const userId = match[1];
if (!repMsg && !userId) { return rep(lang.get('blacklist_unban_err_noparams')) }
return rep (userId ? blacklist.unbanWithUserId(userId) : blacklist.unbanWithMessage(repMsg));
})
/**
* 查看更多管理员命令
* @param {[type]} /\/pwshelp/ [description]
* @param {[type]} ({ rep, msg } [description]
* @return {[type]} [description]
*/
onText(/\/pwshelp/, ({ rep, msg }) => {
if (helper.isPrivate(msg)) { return console.warn('不能运行在Private私聊状态下') }
return rep(lang.get('pwshelp'));
})
/**
* 管理员在审稿群解除用户的会话状态
* @param {[type]} /\/unre (.+)|\/unre/ [description]
* @param {[type]} ({ rep, msg, match, repMsg } [description]
* @return {[type]} [description]
*/
onText(/\/unre (.+)|\/unre/, ({ rep, msg, match, repMsg }) => {
if (helper.isPrivate(msg)) { return console.warn('不能运行在Private私聊状态下') }
let userId = match[1];
if (!repMsg && !userId) { return rep(lang.get('unre_err_noparams')) }
let message = subs.getMsgWithReply(repMsg);
if (!userId) {
if (!message) {
if (!repMsg.forward_from) {
throw {message: lang.get('unre_err_unknown')};// 既没有稿件,回复的也不是转发而来的信息,则报错
} else {
message = { chat: repMsg.forward_from, from: repMsg.forward_from }
}
}
userId = message.from.id;
}
if (!re.has(userId)) {
throw {message: lang.get('unre_err_not_exists')};// 用户不存在于会话列表
}
message ? re.end(message): re.endWithId(userId);
rep(lang.get('unre_success'))
})
/**
* 封锁一个用户
* @param {[type]} /\/ban (.+)|\/ban/ [description]
* @param {[type]} ({msg, match, repMsg, rep} [description]
* @return {[type]} [description]
*/
onText(/\/ban (.+)|\/ban/, ({msg, match, repMsg, rep}) => {
if (helper.isPrivate(msg)) { return false; }
const userId = match[1];
if (!repMsg && !userId) { return rep(lang.get('blacklist_ban_err_noparams')) }
return rep(userId ? blacklist.banWithUserId(userId) : blacklist.banWithMessage(repMsg))
})
/**
* 在审稿群拒绝一个稿件
* @param {[type]} /\/no (.+)|\/no/ [description]
* @param {[type]} async ({ msg, match, rep, repMsg, chatId } [description]
* @return {[type]} [description]
*/
onText(/\/no (.+)|\/no/, async ({ msg, match, rep, repMsg, chatId }) => {
if (helper.isPrivate(msg)) { return false }
const reason = match[1];
if (!reason) {throw {message: lang.get('err_reject_reason')}}// 没有理由则驳回请求
let message = subs.getMsgWithReply(repMsg);
if (!message) { throw {message: lang.get('err_no_sub')} }// 没找到稿件
// 若稿件已经发布,则驳回操作
if (message.receive_date) {
throw {message: lang.get('err_repeat')}
}
await msgControl.rejectMessage(message, msg.from, reason);
rep(lang.get('admin_reject_finish', { reason }));
})
/**
* 在审稿群对用户稿件进行回复
* !只能回复文本
* @param {[type]} /\/re (.+)|\/re/ [description]
* @param {[type]} ({ msg, match, rep, repMsg } [description]
* @return {[type]} [description]
*/
onText(/\/re (.+)|\/re/, p => msgControl.replyMessageWithCommand(p, '/re'))
/**
* 回复用户一些信息,但不进入对话模式
* @param {[type]} /\/echo (.+)|\/echo/ [description]
* @param {[type]} async ({ msg, match, rep, repMsg } [description]
* @return {[type]} [description]
*/
onText(/\/echo (.+)|\/echo/, p => msgControl.replyMessageWithCommand(p, '/echo'))
/**
* 使用评论并采纳稿件
* @param {[type]} /\/ok (.+)|\/ok/ [description]
* @param {[type]} ({msg, match} [description]
* @return {[type]} [description]
*/
onText(/\/ok (.+)|\/ok/, ({ msg, match, rep, repMsg }) => {
if (helper.isPrivate(msg)) { return false; }
const comment = match[1];
let message = subs.getMsgWithReply(repMsg);// 找到稿件
if (!message) { throw {message: lang.get('err_no_sub')} }// 稿件不存在
msgControl.receiveMessage(message, msg.from, { comment });// 采纳稿件
})
/**
* 设置审稿群
* @param {String} /\/setgroup$|\/setgroup@/
*/
onText(/\/setgroup$|\/setgroup@/, ({ msg, chatId, rep }) => {
if (helper.isPrivate(msg)) { return false; }
if (!helper.isAdmin(msg)) {
return console.warn('设置审稿群,但操作者不是配置文件中配置的Admin!');
} else if (!helper.isMe(msg)) {
return console.log('设置审稿群:不是本机器人!');
}
// 设置审稿群
helper.updateConfig({ Group: msg.chat.id });
// 回复用户
rep(lang.get('command_setgroup_tip'))
})
/**
* start命令
* @param {String} /\/start/
*/
onText(/\/start/, ({ msg, rep }) => {
if (!helper.isPrivate(msg)) { return false }// 仅私聊可用
if (helper.isBlock(msg)) { return false }// 被封锁者不可用
if (re.has(msg.from.id)) {
re.end(msg);// 若已经是编辑模式,则退出
}
rep (lang.get('start'));
})
/**
* help命令
* @param {String} /\/help/
*/
onText(/\/version/, ({ rep }) => {
rep(lang.get('help', {ver: '1.0', link: 'https://github.com/axiref/telegram-pwsbot'}));
})
================================================
FILE: src/handler/commandHandler.js
================================================
import { bot, vars, lang } from '../core';
import msgControl from './msgControl';
/**
* onText命令闭包函式
* @param {Object} msg 当前的Msg对象
* @param {Function} cb 回调函数,rep传回一个rep函数,输入文本即可回复当前用户文本;chatId;repMsg回复的信息(若有)
* @return {[type]} [description]
*/
export default function (preg, cb)
{
bot.onText(preg, async (msg, match) => {
try {
const chatId = msg.chat.id;
const repMsg = msg.reply_to_message;
await cb({
/**
* 此函式可直接回复用户指令
* @param {[type]} text [description]
* @return {[type]} [description]
*/
rep: text => {
return msgControl.sendCurrentMessage(text, msg, {
reply_to_message_id: msg.message_id
})
},
chatId,// 会话ID
repMsg,// 被回复的信息
match,// 匹配到的消息
msg// 用户指令消息本身
})
} catch (err) {
let errText = err.message;
if (msg) {
let params = msg ? {reply_to_message_id: msg.message_id} : {}
if (errText == vars.BOT_BLOCK) {
errText = lang.get('re_send_err');
}
msgControl.sendCurrentMessage(errText, msg, params);
}
throw {err, msg};
}
})
}
================================================
FILE: src/handler/msgControl.js
================================================
import {config, bot, vars, lang, subs, helper, re} from '../core';
/**
* (主动)消息发送控制器
* @type {Array}
*/
export default
{
/**
* 向用户发送投稿确认信息
* @param {Object} message Message
* @return {[type]} [description]
*/
subAsk (message) {
let yeslabel = message.forward_date ? lang.get('yes_only') : lang.get('yes');
let inline_keyboard = [[{text: yeslabel, callback_data: vars.SUB_REAL}]];
let reply_to_message_id = (message.message_id) ? message.message_id : message.media[0].message_id;
let text = lang.get('sub_confirm_tip');
if (!message.forward_date) {
// 如果是转发的讯息,则投稿者无权选择匿名
inline_keyboard[0].push({ text: lang.get('no'), callback_data: vars.SUB_ANY });
} else {
// 投稿者转发别处的消息,不显示否按钮,并且文案也有所不同
text = lang.get('sub_confirm_tip_fwd');
}
inline_keyboard.push([{ text: lang.get('sub_button_cancel'), callback_data: vars.SUB_CANCEL }]);
bot.sendMessage(message.chat.id, text, {
reply_to_message_id,
reply_markup: { inline_keyboard }
})
},
/**
* 编辑一条信息
* @param {[type]} text [description]
* @param {[type]} params {message_id, chat_id}
* @return {[type]} [description]
*/
editMessage (text, params = {}) {
let _params = Object.assign({
parse_mode: 'Markdown',
}, params)
return bot.editMessageText(text, _params);
},
/**
* 编辑当前的消息
* @param {[type]} text [description]
* @param {[type]} message [description]
* @param {Object} params [description]
* @return {[type]} [description]
*/
editCurrentMessage (text, message, params = {}) {
let _params = Object.assign({
chat_id: message.chat.id,
message_id: message.message_id
}, params);
return this.editMessage(text, _params);
},
/**
* 使用现有结构发送消息
* @param {[type]} text [description]
* @param {[type]} message [description]
* @param {Object} params [description]
* @return {[type]} [description]
*/
sendCurrentMessage (text, message, params = {}) {
return this.sendMessage(message.chat.id, text, params);
},
/**
* 发送消息,默认使用markdown
* @param {[type]} chatId [description]
* @param {[type]} text [description]
* @param {[type]} params [description]
* @return {[type]} [description]
*/
sendMessage (chatId, text, params) {
let _params = Object.assign({
parse_mode: 'Markdown',
}, params)
return bot.sendMessage(chatId, text, _params);
},
/**
* 将消息转发到审稿群
* @param {Object} reply_to_message Message
* @param {String} type 投稿类型
* @return {Promise} 成功后返回转发后的Message对象
* {reply_to_message_id: 审稿群actionMsg应该回复的稿件消息ID,message,稿件}
*/
async forwardMessage (reply_to_message, type) {
let condition = subs.getMsgCondition(reply_to_message);
let message = subs.one(condition);
let fwdMsg = {}, respMsg = {};
// 若是mediagroup消息
if (message.media_group_id) {
fwdMsg = (await bot.sendMediaGroup(config.Group, message.media))[0];
// 将审稿群的mediagroupId写到mediaGroup消息的fwdMsgGroupId节点
respMsg = subs.update(condition, {fwdMsgGroupId: fwdMsg.media_group_id, sub_type: type});
} else {
// 附加审稿群的消息ID到稿件
fwdMsg = await bot.forwardMessage(config.Group, message.chat.id, message.message_id);// 转发至审稿群
respMsg = subs.update(condition, {fwdMsgId: fwdMsg.message_id, sub_type: type});
}
return {reply_to_message_id: fwdMsg.message_id, message: respMsg};
},
/**
* 询问管理员如何处理稿件
* @param {Object} {reply_to_message_id: 审稿群actionMsg应该回复的稿件消息ID,message,稿件}
* @return {[type]} [description]
*/
async askAdmin ({reply_to_message_id, message}) {
let condition = subs.getMsgCondition(message);
let text = lang.getAdminAction(message);
let from = message.sub_type == vars.SUB_ANY ? 'anonymous' : 'real';
let actionMsg = await bot.sendMessage(config.Group, text, {
reply_to_message_id,
parse_mode: 'Markdown',
disable_web_page_preview: true,
reply_markup: {
resize_keyboard: true,
inline_keyboard: [[{text: lang.get('button_receive'), callback_data: `receive:${from}`}]]
}
});
subs.update(condition, {actionMsgId: actionMsg.message_id});// 更新actionMsgId
},
/**
* 推送频道消息
* @param {[type]} message [description]
* @param {[type]} params comment=评论, isMute=是否静音
* @return {[type]} [description]
*/
async sendChannel (message, params) {
let resp = null;
let caption = subs.getCaption(message, params);
let options = subs.getOptions(message, caption, params);
if (message.media_group_id) {
message.media[0].caption = caption;
resp = await bot.sendMediaGroup(config.Channel, message.media);
} else if (message.audio) {
resp = await bot.sendAudio(config.Channel, message.audio.file_id, options);
} else if (message.document) {
resp = await bot.sendDocument(config.Channel, message.document.file_id, options);
} else if (message.voice) {
resp = await bot.sendVoice(config.Channel, message.voice.file_id, options);
} else if (message.video) {
resp = await bot.sendVideo(config.Channel, message.video.file_id, options);
} else if (message.photo) {
resp = await bot.sendPhoto(config.Channel, message.photo[0].file_id, options);
} else {
resp = await bot.sendMessage(config.Channel, caption, options)
}
return resp;
},
/**
* 审核稿件
* @param {Object} message 稿件,查询出来的
* @param {Object} receive 审稿人对象,一般是message.from
* @param {String} comment 附加评论
* @param {Boolean} isMute 是否静音推送
*/
async receiveMessage (message, receive, params = {}) {
// 若稿件已经发布,则驳回操作
if (message.receive_date) {
return bot.sendMessage(config.Group, lang.get('err_repeat'), {
reply_to_message_id: message.fwdMsgId
})
}
if (helper.isMute()) {params.isMute = true}
let resp = await this.sendChannel(message, params);
let condition = subs.getMsgCondition(message);
// 记录审稿人和时间
message = subs.update(condition, { receive, receive_date: helper.getTimestamp(), receive_params: params })
// 获取审稿群通过审核文案
let text = lang.getAdminActionFinish(message);
// 编辑审稿群actionMsg
await this.editMessage(text, {chat_id: config.Group, message_id: message.actionMsgId, disable_web_page_preview: true});
let reply_to_message_id = subs.getReplytoMessageId(message);
// 向用户发送稿件过审信息
await bot.sendMessage(message.chat.id, lang.get('sub_finish_tip'), { reply_to_message_id })
return resp;
},
/**
* 拒绝投稿
* @param {[type]} message [description]
* @param {Object} reject 是谁操作的
* @param {String} reason 理由
* @return {[type]} [description]
*/
async rejectMessage (message, reject, reason) {
// 若稿件已经拒绝,则驳回
if (message.reject_date) {
return bot.sendMessage(config.Group, lang.get('err_repeat_reject'), {
reply_to_message_id: message.fwdMsgId
})
}
let condition = subs.getMsgCondition(message);
// 记录操作人和拒绝理由及时间
message = subs.update(condition, { reject, reject_date: helper.getTimestamp(), reject_reason: reason })
let rejectText = lang.get('reject_tips', { reason });
// 获取审稿群拒绝审核文案
let text = lang.getAdminActionReject(message, reason);
// 编辑审稿群actionMsg
let reply_to_message_id = subs.getReplytoMessageId(message);
await this.editMessage(text, {chat_id: config.Group, message_id: message.actionMsgId, disable_web_page_preview: true})
await bot.sendMessage(message.chat.id, rejectText, { reply_to_message_id, parse_mode: 'Markdown' });
return message;
},
/**
* 回复用户消息,同时用户将进入聊天状态
* 用户可透过KeyboardButton退出聊天,管理员可透过/endre 结束会话
* @param {[type]} message 稿件
* @param {String} comment 管理员回复给用户的消息
* @return {[type]} [description]
*/
async replyMessage (message, comment, reMode = true) {
if (reMode) {
await re.start(message);// 进入会话模式
}
await this.sendCurrentMessage(lang.get('re_comment', { comment }), message);
return true;
},
/**
* 回复用户信息
* @param {[type]} options.msg [description]
* @param {[type]} options.match [description]
* @param {[type]} options.rep [description]
* @param {[type]} options.repMsg [description]
* @param {String} command /re 或者 /echo
* re 会进入会话状态, echo 只是发送,不进入会话
* @return {[type]} [description]
*/
async replyMessageWithCommand ({ msg, match, rep, repMsg }, command = '/re') {
if (helper.isPrivate(msg)) { return false }
const comment = match[1];
if (!comment) {throw {message: lang.get('admin_reply_err', { command })}}// 没有输入消息
let message = subs.getMsgWithReply(repMsg);
if (!message && !repMsg.forward_from) { return false }// 无从回复
if (!message) { message = { chat: repMsg.forward_from, from: repMsg.forward_from } }
let chatMode = command == '/re' ? true : false;
await this.replyMessage(message, comment, chatMode);
let respMsg = await rep(lang.get('re_send_success'));
await helper.sleep(1000);
this.editCurrentMessage("...", respMsg);
await helper.sleep(2000);
bot.deleteMessage(respMsg.chat.id, respMsg.message_id);
},
/**
* 管理员点击采纳稿件(从actionMsg点击按钮)
* @param {Object} query callback data
* @return {Promise} [description]
*/
async receive (query) {
let fwdMsg = query.message.reply_to_message;// 审稿群的稿件
let condition = subs.getFwdMsgCondition(fwdMsg);// 得到查询条件
let message = subs.one(condition);// 得到真实稿件
this.receiveMessage(message, query.from);
}
}
================================================
FILE: src/handler/msgHandler.js
================================================
import {config, bot, helper, lang, subs, blacklist, re} from '../core';
import msgControl from './msgControl';
/**
* 接收到来自Telegram的普通私聊消息
* @type {[type]}
*/
export default
{
process (message) {
subs.optimize();// 利用消息事件主动触发优化函式
this.message = message;
if (helper.isCommand(message)) {
return false;// 此处不处理command
} else if (helper.isNewJoin(message)) {
let chatId = message.chat.id;
// 是否是本机器人新进群的提示信息
if (config.Group && config.Group != chatId) {
return msgControl.sendMessage(chatId, lang.get('reject_intro_tips')).then(() => {
bot.leaveChat(chatId);
})
}
bot.sendMessage(message.chat.id, lang.get('intro_new_group', {command: '/setgroup'}));
} else if (helper.isPrivate(message)) {
// 是私信渠道的投稿
if (helper.isBlock(message, true)) { return false }
// 如果用户发送了 "结束对话"
if (message.text && message.text == lang.get('re_end')) {
return re.end(message);
} else if (re.has(message.from.id)) {
// 进入会话模式,将用户之所有讯息转发到审稿群
bot.forwardMessage(config.Group, message.chat.id, message.message_id);// 转发至审稿群
} else {
subs.process(message, (message) => { msgControl.subAsk(message) });
}
}
}
}
================================================
FILE: src/handler/queryHandler.js
================================================
import {config, bot, vars, lang, helper} from '../core';
import msgControl from './msgControl';
/**
* 点击actionMsg后会产生回调函式
* @type
*/
export default
{
async process (query) {
try {
const actionMsg = query.message;// 操作的actionMsg
const data = query.data;
if (this.isAdminReceiveAction(data)) { await msgControl.receive(query) }
else { this.processSubmission(data, actionMsg) }
bot.answerCallbackQuery(query.id)
} catch (err) {
if (err.message == vars.BOT_NOAUTH_KICK) {
err.message = lang.get('err_no_auth_kick')
} else if (err.message == vars.BOT_NOAUTH) {
err.message = lang.get('err_no_auth')
}
bot.answerCallbackQuery(query.id, { text: err.message, show_alert: true })
throw err;
}
},
/**
* 处理用户点击投稿事件
* @param {String} type 投稿类型,vars_SUB*
* @param {Object} actionMsg ActionMsg 动作信息
*/
async processSubmission (type, actionMsg) {
let message = actionMsg.reply_to_message;// 稿件
if (helper.isBlock(message, true)) {
return false;
}
if (type == vars.SUB_CANCEL) {
// 点击取消投稿
return msgControl.editCurrentMessage(lang.get('sub_cancel_tip'), actionMsg);
}
msgControl.editCurrentMessage(lang.get('sub_submit_tip'), actionMsg);
let resp = await msgControl.forwardMessage(message, type);// 转发到审稿群
msgControl.askAdmin(resp);// 询问管理员如何操作
},
/**
* 是管理员点击了采纳吗
* @param {String} data query.data
* @return {Boolean}
*/
isAdminReceiveAction (data) {
return (data == vars.REC_ANY || data == vars.REC_REAL) ? true : false;
}
}
================================================
FILE: src/lang/zh-CN.json
================================================
{
"sub_new": "新投稿",
"sub_people": "投稿人: [{{username}}](tg://user?id={{userid}})",
"sub_from": "来源: [{{username}}](tg://user?id={{userid}})",
"sub_from_channel": "来源:[{{channel}}](https://t.me/{{username}}/{{id}}) (频道)",
"sub_from_reserve": "保留来源:{{reserve}}",
"sub_button_cancel": "取消投稿",
"sub_confirm_tip": "即将完成投稿...\n 您是否想要保留消息来源 \n (保留消息发送者用户名)",
"sub_confirm_tip_fwd": "确认投稿吗?",
"sub_cancel_tip": "已取消投稿。",
"sub_submit_tip": "感谢您的投稿,我们会稍后通知您结果",
"sub_from_channel_private": "来源: 私有频道 **{{channel}}** ",
"sub_finish_tip": "您的稿件已过审,感谢您的支持!",
"from_anonymous": "匿名",
"from_real": "保留",
"admin_morehelp": "更多帮助: {{command}}",
"admin_reader": "审稿人: [{{username}}](tg://user?id={{userid}})",
"admin_finish_label": "已投稿",
"admin_reject_label": "已拒绝,理由:\n`{{reason}}`",
"admin_reject": "操作人: [{{username}}](tg://user?id={{userid}})",
"button_receive": "采用",
"command_setgroup_tip": "已成功设定本群为审稿群!",
"intro_new_group": "欢迎,使用 {{command}} 命令将此群设置为审稿群",
"via_user": "via [{{username}}](tg://user?id={{userid}})",
"via_channel": "via [{{channel}}](https://t.me/{{username}}/{{id}})",
"via_channel_private": "via **{{channel}}**",
"comment_label": "*小编:* {{comment}}",
"err_no_auth": "很抱歉,机器人没有拥有频道管理员权限!\n请直接将机器人添加为频道管理员,然后再试",
"err_no_auth_kick": "很抱歉,机器人没有拥有频道管理员权限!\n请将机器人从频道黑名单放出来,然后再试",
"err_reject_reason": "很抱歉,礼貌起见需要 `/no <拒绝理由>`才能拒绝稿件",
"admin_reject_finish": "已经拒绝此稿件,理由: \n`{{reason}}`",
"start": "可接收的投稿类型: \n文字 \n图片 \n音频/语音 \n视频 \n文件",
"err_no_sub": "很抱歉,没有找到稿件!",
"err_repeat": "此稿件已经发表,请勿重复操作",
"err_repeat_reject": "此稿件已经拒绝,请勿重复操作",
"reject_tips": "*稿件退回*\n理由: `{{reason}}`",
"help": "Telegram pwsbot \n频道投稿机器人\nv{{ver}}\n {{link}}",
"blacklist_exists": "很抱歉,[{{username}}](tg://user?id={{userid}}) 已经被拉黑,无需重复操作",
"blacklist_exists_only_id": "很抱歉,ID为`{{id}}`的用户已经被拉黑,无需重复操作",
"blacklist_success": "用户 [{{username}}](tg://user?id={{userid}}) 已经被拉黑,解除拉黑使用`/unban`命令",
"blacklist_success_only_id": "ID为`{{id}}`的用户已经被拉黑,解除拉黑使用`/unban {{id}}`命令",
"blacklist_unban": "用户 [{{username}}](tg://user?id={{userid}}) 已经解除黑名单。",
"blacklist_unban_err_noparams": "`/unban`命令需要一个参数,往往是用户ID,或者您可以回复稿件`/unban`命令,即可解除对该稿件投稿者的封锁。",
"blacklist_ban_err_noparams": "`/ban`命令需要一个参数,往往是用户ID,或者您可以回复稿件`/ban`命令,即可封掉投稿者,被封锁的人无法继续投稿。",
"blacklist_unban_only_id": "ID为`{{id}}`的用户已经解除黑名单。",
"blacklist_ban_tips": "很抱歉,本机器人无法继续为您提供服务,因为您已遭到封锁,建议联系频道管理员解决。",
"sub_not_exists": "很抱歉,查找不到对应的稿件,操作无法完成",
"blacklist_unban_notexists": "抱歉,黑名单里没有TA",
"admin_finish_comment": "*评语*:\n{{comment}}",
"admin_reply_err": "`{{command}} <回复内容>` 命令需要对稿件回复,回复内容是必不可少的,投稿者将收到您的回复",
"re_end": "结束对话",
"re_start": "*系统提示: *您已进入对话",
"re_comment": "*管理员: * {{comment}}",
"re_end_tips": "*系统提示: *您已退出对话",
"unre_err_noparams": "`/unre` 命令需要对稿件回复,或者输入一个用户ID,将解除该用户的会话状态,该用户可继续投稿",
"unre_err_not_exists": "该用户并没有处于会话状态,他可继续投稿",
"unre_success": "已解除该用户的会话状态,他可继续投稿",
"re_err_unknown": "抱歉,不知道您想回复给谁",
"unre_err_unknown": "抱歉,不知道您想给谁解除会话状态",
"re_send_success": "您已成功回复该用户",
"re_send_err": "很抱歉消息发送失败,因为本机器人被用户封掉了!",
"reject_intro_tips": "很抱歉,这个群并非审稿群,如需重新设置审稿群,您应该先删除配置文件中的`Group`字段\n\n🤖: *告辞*",
"pwshelp": "您可对稿件采取下列操作: \n- `/ok <评论>` 采纳稿件\n- `/no <理由>` 拒绝稿件\n- `/re <内容>` 回复投稿者\n- `/ban` 拉黑投稿者\n- `/unban` 取消拉黑投稿者\n- `/unre` 结束用户的会话状态\n- `/echo <内容>` 回复用户而不进入会话模式",
"yes": "保留",
"yes_only": "是",
"no": "匿名"
}
================================================
FILE: src/lang/zh-TW.json
================================================
{
"sub_new": "新投稿",
"sub_people": "投稿人: [{{username}}](tg://user?id={{userid}})",
"sub_from": "來源: [{{username}}](tg://user?id={{userid}})",
"sub_from_channel": "來源:[{{channel}}](https://t.me/{{username}}/{{id}}) (頻道)",
"sub_from_reserve": "保留來源:{{reserve}}",
"sub_button_cancel": "取消投稿",
"sub_confirm_tip": "即將完成投稿...\n 您是否想要保留消息來源 \n (保留消息發送者使用者名稱)",
"sub_confirm_tip_fwd": "確認投稿嗎?",
"sub_cancel_tip": "已取消投稿。",
"sub_submit_tip": "感謝您的投稿,我們會稍後通知您結果",
"sub_from_channel_private": "來源: 私有頻道 **{{channel}}** ",
"sub_finish_tip": "您的稿件已過審,感謝您的支持!",
"from_anonymous": "匿名",
"from_real": "保留",
"admin_morehelp": "更多幫助: {{command}}",
"admin_reader": "審稿人: [{{username}}](tg://user?id={{userid}})",
"admin_finish_label": "已投稿",
"admin_reject_label": "已拒絕,理由:\n`{{reason}}`",
"admin_reject": "操作人: [{{username}}](tg://user?id={{userid}})",
"button_receive": "採用",
"command_setgroup_tip": "已成功設定本群為審稿群!",
"intro_new_group": "歡迎,使用 {{command}} 命令將此群設置為審稿群",
"via_user": "via [{{username}}](tg://user?id={{userid}})",
"via_channel": "via [{{channel}}](https://t.me/{{username}}/{{id}})",
"via_channel_private": "via **{{channel}}**",
"comment_label": "*小編:* {{comment}}",
"err_no_auth": "很抱歉,機器人沒有擁有頻道管理員權限!\n請直接將機器人添加為頻道管理員,然後再試",
"err_no_auth_kick": "很抱歉,機器人沒有擁有頻道管理員權限!\n請將機器人從頻道黑名單放出來,然後再試",
"err_reject_reason": "很抱歉,禮貌起見需要 `/no <拒絕理由>`才能拒絕稿件",
"admin_reject_finish": "已經拒絕此稿件,理由: \n`{{reason}}`",
"start": "可接收的投稿類型: \n文字 \n圖片 \n音訊/語音 \n影片 \n文件",
"err_no_sub": "很抱歉,沒有找到稿件!",
"err_repeat": "此稿件已經發表,請勿重複操作",
"err_repeat_reject": "此稿件已經拒絕,請勿重複操作",
"reject_tips": "*稿件退回*\n理由: `{{reason}}`",
"help": "Telegram pwsbot \n頻道投稿機器人\nv{{ver}}\n {{link}}",
"blacklist_exists": "很抱歉,[{{username}}](tg://user?id={{userid}}) 已經被黑名單,無需重複操作",
"blacklist_exists_only_id": "很抱歉,ID為`{{id}}`的用戶已經被黑名單,無需重複操作",
"blacklist_success": "用戶 [{{username}}](tg://user?id={{userid}}) 已經被黑名單,解除黑名單使用`/unban`命令",
"blacklist_success_only_id": "ID為`{{id}}`的用戶已經被黑名單,解除黑名單使用`/unban {{id}}`命令",
"blacklist_unban": "用戶 [{{username}}](tg://user?id={{userid}}) 已經解除黑名單。",
"blacklist_unban_err_noparams": "`/unban`命令需要一個參數,往往是用戶ID,或者您可以回復稿件`/unban`命令,即可解除對該稿件投稿者的封鎖。",
"blacklist_ban_err_noparams": "`/ban`命令需要一個參數,往往是用戶ID,或者您可以回復稿件`/ban`命令,即可封掉投稿者,被封鎖的人無法繼續投稿。",
"blacklist_unban_only_id": "ID為`{{id}}`的用戶已經解除黑名單。",
"blacklist_ban_tips": "很抱歉,本機器人無法繼續為您提供服務,因為您已遭到封鎖,建議聯繫頻道管理員解決。",
"sub_not_exists": "很抱歉,查找不到對應的稿件,操作無法完成",
"blacklist_unban_notexists": "抱歉,黑名單裡沒有TA",
"admin_finish_comment": "*評語*:\n{{comment}}",
"admin_reply_err": "`{{command}} <回復內容>` 命令需要對稿件回復,回復內容是必不可少的,投稿者將收到您的回覆",
"re_end": "結束對話",
"re_start": "*系統提示: *您已進入對話",
"re_comment": "*管理員: * {{comment}}",
"re_end_tips": "*系統提示: *您已退出對話",
"unre_err_noparams": "`/unre` 命令需要對稿件回復,或者輸入一個用戶ID,將解除該用戶的會話狀態,該用戶可繼續投稿",
"unre_err_not_exists": "該用戶並沒有處於會話狀態,他可繼續投稿",
"unre_success": "已解除該用戶的會話狀態,他可繼續投稿",
"re_err_unknown": "抱歉,不知道您想回復給誰",
"unre_err_unknown": "抱歉,不知道您想給誰解除會話狀態",
"re_send_success": "您已成功回復該用戶",
"re_send_err": "很抱歉消息發送失敗,因為本機器人被用戶封掉了!",
"reject_intro_tips": "很抱歉,這個群並非審稿群,如需重新設置審稿群,您應該先刪除配置文件中的`Group`欄位\n\n🤖: *告辭*",
"pwshelp": "您可對稿件採取下列操作: \n- `/ok <評論>` 採納稿件\n- `/no <理由>` 拒絕稿件\n- `/re <內容>` 回復投稿者\n- `/ban` 黑名單投稿者\n- `/unban` 取消黑名單投稿者\n- `/unre` 結束用戶的會話狀態\n- `/echo <內容>` 回復用戶而不進入會話模式",
"yes": "保留",
"yes_only": "是",
"no": "匿名"
}
================================================
FILE: src/main.js
================================================
import { bot } from '././core';
import queryHandler from './handler/queryHandler'
import botCommand from './handler/botCommand'
import msgHandler from './handler/msgHandler'
// setting up the message handler
bot.on('message', (message) => { msgHandler.process(message) });
// set up the asynchronous callback handler
bot.on('callback_query', (query) => { queryHandler.process(query) });
// setting error handler
bot.on('polling_error', (error) => { throw error; });
console.log("Server is running...");
if (process.env.BOT_ENV == 'test') {
setTimeout( () => {
console.log('Exiting automatically...');
process.exit(0);
}, 3000)
}
================================================
FILE: src/model/BlackList.js
================================================
import { lang, vars, helper, subs } from '../core';
import Db from './Db';
class BlackList extends Db
{
/**
* 取消封锁一个用户,透过ID
* @param {Int} userId 用户ID
* @return {String} 成功返回文案,失败抛出异常
*/
unbanWithUserId (userId) {
let condition = {id: userId};
if (this.has(condition)) {
this.del(condition);
return lang.get('blacklist_unban_only_id', condition);
}
throw {message: lang.get('blacklist_unban_notexists')}
};
/**
* 从管理员的指令消息包含的稿件里解除封锁某个用户
* @param {[type]} repMsg 用户用 /unban 命令回复的那个消息
* @return {String} 成功返回文案,失败抛出异常
*/
unbanWithMessage (repMsg) {
let message = subs.getMsgWithReply(repMsg);
if (!message) {throw { message: lang.get('sub_not_exists') }};
let condition = {id: message.from.id};
let userinfo = lang.getUser(message.from);
// 若用户已经被封锁
if (blacklist.has(condition)) {
blacklist.del(condition);
return lang.get('blacklist_unban', userinfo);
}
throw {message: lang.get('blacklist_unban_notexists')}
};
/**
* 透过UserID封锁用户
* @param {[type]} userId [description]
* @return {[type]} [description]
*/
banWithUserId (userId) {
let condition = {id: userId};
if (this.has(condition)) {
throw {message: lang.get('blacklist_exists_only_id', condition)}
}
this.add(condition);
return lang.get('blacklist_success_only_id');
};
/**
* 透过用户指令来封锁用户
* @param {[type]} repMsg 用户用 /unban 命令回复的那个消息
* @return {[type]} [description]
*/
banWithMessage (repMsg) {
let message = subs.getMsgWithReply(repMsg);
if (!message) { throw { message: lang.get('sub_not_exists') }}
let userinfo = lang.getUser(message.from);
// 若用户已经被封锁
if (this.has({id: message.from.id})) {
throw {message: lang.get('blacklist_exists', userinfo)}
}
this.add(message.from);
return lang.get('blacklist_success', userinfo);
};
}
const blacklist = new BlackList('blacklist');
export default blacklist;
================================================
FILE: src/model/Db.js
================================================
import low from 'lowdb';
import FileSync from 'lowdb/adapters/FileSync';
const adapter = new FileSync('db.json')
const db = low(adapter)
db.defaults({ re: [], blacklist: [], subs: []}).write();
class Db
{
/**
* 获取DB对象
* @return {lowdb}
*/
get db () {
return db;
};
/**
* 创建一个DB对象
* @param {String} table 表名
*/
constructor (table) {
this.table = table;
};
/**
* 添加一行记录
* @param {Object} data 参数
*/
add (data) {
db.get(this.table).push(data).write();
return data;
};
/**
* 是否存在某记录
* @param {[type]} data [description]
* @return {Boolean} [description]
*/
has (data) {
return this.get(data) ? true : false;
};
/**
* 获取符合条件的记录
* @param {[type]} data [description]
* @return {[type]} [description]
*/
get (data) {
let row = db.get(this.table).filter(data).value();
if (Array.isArray(row) && row.length === 0) {
return null;
}
return row;
};
/**
* 查找一个记录
* @param {[type]} data [description]
* @return {[type]} [description]
*/
one (data) {
return db.get(this.table).find(data).value();
};
/**
* 删除符合条件的记录
* @param {[type]} data [description]
* @return {[type]} [description]
*/
del (data) {
db.get(this.table).remove(data).write();
};
/**
* 更新符合条件的记录
* @param {Object} condition 查询条件
* @param {Object} data 要更新的数据
* @return {[type]} [description]
*/
update (condition, data) {
return db.get(this.table).find(condition).assign(data).write()
};
}
export default Db;
================================================
FILE: src/model/Message.js
================================================
import { lang, vars, helper, re } from '../core';
import Db from './Db';
class Message extends Db
{
header = 0;
/**
* 存储Message
* 当处理完毕时,会调用回调函式传回消息
* @param {Object} message Message
* @param {Function} callback [description]
* @return {[type]} [description]
*/
process (message, callback) {
// 针对MediaGroup
if (message.media_group_id) {
let groupMsg = this.processMediaMessage(message);
clearTimeout(this.handler);
this.handler = setTimeout(() => callback(groupMsg), 500);
} else {
this.push(message);
callback(message);
}
};
/**
* 获取回复的MessageID
* @param {[type]} message [description]
* @return {[type]} [description]
*/
getReplytoMessageId (message) {
return message.media_group_id ? message.media[0].message_id : message.message_id;
};
/**
* 获取Caption文本
* @param {Object} message 稿件
* @param {String} comment 评论
* @return {String} 返回Caption文本,同样可以使用在SendTextMessage
*/
getCaption (message, params) {
let comment = params ? params.comment : '';
let caption = message.caption ? message.caption + "\n" : '';
// 添加评语
if (comment) {caption += lang.get('comment_label', { comment }) + "\n" }
// 若是实名投稿,则附加版权信息
if (message.sub_type == vars.SUB_REAL) { caption += "\n" + lang.getViaInfo(message) }
if (message.text) {
caption = message.text + `\n\n${caption}`;
}
return caption;
};
/**
* 获取sendChannel的Option额外参数
* @param {[type]} message [description]
* @param {[type]} caption [description]
* @param {[type]} options.comment [description]
* @param {[type]} options.isMute [description]
* @return {[type]} [description]
*/
getOptions (message, caption, params) {
let comment = params ? params.comment : '';
let isMute = params ? params.isMute : false;
// 支援markdown和默认关闭链接预览
let options = {parse_mode: 'Markdown', disable_web_page_preview: true, caption}
// 若文本或comment含有URL,则开启链接预览
if (helper.hasUrl(message.text) || helper.hasUrl(comment)) { options.disable_web_page_preview = false }
if (isMute) {options.disable_notification = true}
return options;
};
/**
* 排除一些key,得到一个新对象
* @param {[type]} obj [description]
* @param {[type]} keys [description]
* @return {[type]} [description]
*/
withoutKeys (obj, keys) {
let nobj = Object.assign({}, obj);
for (let key of keys) {
delete nobj[key];
}
return nobj
};
/**
* 合并mediaGroup消息为新的格式
* @param {[type]} message [description]
* @return {[type]} [description]
*/
processMediaMessage (message) {
let groupMsg = this.one({media_group_id: message.media_group_id});
if (!groupMsg) {
// 排除这些键
groupMsg = this.withoutKeys(message, ['caption', 'photo', 'video', 'message_id']);
groupMsg.media = [];
} else {
// 若已经存在此项,则返回
let ids = Object.keys(groupMsg.media).map(f=>groupMsg.media[f].message_id);
if (ids.includes(message.message_id)) { return groupMsg }
}
let mediaRow = {};
if (message.photo) {
// 选择最清晰的那张图
mediaRow = { message_id: message.message_id, type: 'photo', media: message.photo[message.photo.length - 1].file_id }
} else if (message.video) {
mediaRow = { message_id: message.message_id, type: 'video', media: message.video.file_id }
}
if (message.caption) {
mediaRow.caption = message.caption
}
mediaRow.parse_mode = 'Markdown';
groupMsg.media.push(mediaRow);
// 第一个媒体的描述将作为稿件描述
if (groupMsg.media[0].caption) {
groupMsg.caption = groupMsg.media[0].caption;
}
this.pushMediaGroup(groupMsg);
return groupMsg;
};
/**
* 从审稿群的稿件消息得到原稿件对象的查询条件
* @param {[type]} fwdMsg 审稿群的稿件
* @return {[type]} [description]
*/
getFwdMsgCondition (fwdMsg) {
if (fwdMsg.media_group_id) {
return {fwdMsgGroupId: fwdMsg.media_group_id};
} else if (fwdMsg.message_id) {
return {fwdMsgId: fwdMsg.message_id}
} else {
throw new Error('getMsgFromFwd: 很抱歉,message消息格式不正确!');
}
};
/**
* 从稿件中得到原始稿件的查询条件
* @param {[type]} message [description]
* @return {[type]} [description]
*/
getMsgCondition (message) {
if (message.media_group_id) {
return {media_group_id: message.media_group_id};
} else if (message.message_id) {
return {message_id: message.message_id}
} else {
throw new Error('getMsgFromFwd: 很抱歉,message消息格式不正确!');
}
};
/**
* 透过用户回复的命令来获取稿件
* @param {[type]} fwdMsg 审稿群中的信息
* @return {[type]} [description]
*/
getMsgWithReply (fwdMsg) {
if (!fwdMsg) {return false}
let media_group_id = fwdMsg.media_group_id;
let message_id = fwdMsg.message_id;
let message = null;
if (media_group_id) {
message = this.one({ fwdMsgGroupId: media_group_id });
} else {
message = this.one({fwdMsgId: message_id}) || this.one({actionMsgId: message_id})
}
return message;
};
/**
* 定时优化掉过期的数据
* @return {[type]} [description]
*/
optimize () {
// 计算出10天前的时间戳
let tenday = helper.getTimestamp() - (60*60*24*10)
let msgs = this.db.get(this.table).filter(e => {
return e.date < tenday
}).value();
for (let e of msgs) {
this.del(e);
}
};
/**
* 保存一个MediaGroup
* @param {[type]} groupMsg
* @return {[type]} [description]
*/
pushMediaGroup (groupMsg) {
let condition = {media_group_id: groupMsg.media_group_id};
if (!this.has(condition)) { this.add(groupMsg) } else { this.update(condition, groupMsg) }
};
/**
* 保存一个Message
* @param {[type]} message [description]
*/
push (message) {
if (!this.has({message_id: message.message_id})) {
this.add(message);
}
};
}
export default Message;
================================================
FILE: src/model/Re.js
================================================
import { lang, vars, helper, subs } from '../core';
import msgControl from '../handler/msgControl';
import Db from './Db';
class Re extends Db
{
/**
* 结束对话模式
* @param {[type]} message [description]
* @return {[type]} [description]
*/
end (message) {
this.del({ id: message.from.id });
msgControl.sendCurrentMessage(lang.get('re_end_tips'), message, {
reply_markup: { remove_keyboard: true }
});
};
/**
* 透过userID结束会话
* @param {[type]} userId [description]
* @return {[type]} [description]
*/
endWithId (userId) {
let id = parseInt(userId)
this.end({
from: { id },
chat: { id },
})
};
has (userId) {
let uid = parseInt(userId)
return super.has({ id: uid });
};
/**
* 进入对话模式
* @param {[type]} message [description]
* @return {[type]} [description]
*/
async start (message) {
let condition = { id: message.from.id };
if (!this.has(message.from.id)) {
this.add(condition);
// 给用户发送含有KeyboardButton的消息,告知已进入会话模式
await msgControl.sendCurrentMessage(lang.get('re_start', {re_end: lang.get('re_end')}), message, {
resize_keyboard: true,
one_time_keyboard: true,
reply_markup: {keyboard: [[{text: lang.get('re_end')}]] }
})
return true;
}
return true;
};
}
const re = new Re('re');
export default re;
================================================
FILE: src/utils/Lang.js
================================================
/**
* 用于翻译文案的工具
*/
class Lang {
langName = null;
lang = null;
constructor (langName, vars) {
this.langName = langName;
this.vars = vars;
};
/**
* 获取语言包JSON
* @return {[type]} [description]
*/
get language () {
if (!this.lang) {
this.lang = require(`../lang/${this.langName}.json`);
}
return this.lang;
};
/**
* 得到翻译内容
* @param {String} key 键名,对于Langjson
* @param {Object} context 包含变数的对象
* @return {[type]} [description]
*/
get (key, context) {
let text = this.language[key];
if (!text) {
throw new Error(`不存在的Key,语言:${this.langName},键:${key},请检查翻译文件!`);
}
return text.replace(/{{(.*?)}}/g, (match, key) => context[key.trim()]);
};
/**
* 获取管理员准许投稿后,审稿群的actionMsg文案
* @param {Object} message Message 稿件
* @return {[type]} [description]
*/
getAdminActionFinish (message) {
let text = this.getAdminCommonHeader(message);
text += "\n" + this.getAdminreader(message);
text += "\n" + this.get('admin_finish_label');
let comment = this.getAdminComment(message)
if (comment) {
text += "\n\n" + this.get('admin_finish_comment', { comment });
}
return text;
};
/**
* 获取管理员对消息的评语
* @param {[type]} message [description]
* @return {string} 存在则返回,没有则返回空
*/
getAdminComment (message) {
let params = message.receive_params;
return params ? params.comment : false;
};
getAdminActionReject (message, reason) {
let text = this.getAdminCommonHeader(message);
text += "\n" + this.getAdminReject(message);
text += "\n" + this.get('admin_reject_label', { reason });
return text;
};
getAdminReject (message) {
let userinfo = this.getUser(message.reject);
return this.get('admin_reject', userinfo);
};
getAdminreader (message) {
let userinfo = this.getUser(message.receive);
return this.get('admin_reader', userinfo);
};
/**
* 获取推送到频道内容的页脚版权文本
* via xxxx
* @param {[type]} message [description]
* @return {[type]} [description]
*/
getViaInfo (message) {
let msgInfo = this.getMessageFwdFromInfo(message);
let text = '';
if (msgInfo.type == 'channel_private') {
text = this.get('via_channel_private', msgInfo)
} else if (msgInfo.type == 'channel') {
text = this.get('via_channel', msgInfo)
} else if (msgInfo.type == 'forward_user' || msgInfo.type == 'user') {
text = this.get('via_user', msgInfo);
}
return text;
};
/**
* 获取投稿人username, userid
* @param {[type]} message [description]
* @return {Object} {username, userid}
*/
getUser (user) {
let lastName = user.last_name || '';
let firstName = user.first_name || '';
let username = firstName + ' ' + lastName;
if (!username) {
if (user.username) {
username = user.username;
} else {
username = 'NoName';
}
}
let userid = user.id;
return {username, userid}
};
/**
* 获取转发信息的来源
* 是转发个人的,还是频道的,还是私人频道的,得到这个信息
* @param {[type]} message [description]
* @return {[type]} [description]
*/
getMessageFwdFromInfo (message) {
let resp = {};
// 投稿者转发频道
let fwdChannel = message.forward_from_chat;
let fwdUser = message.forward_from;
let user = message.from;
if (fwdChannel) {
let username = fwdChannel.username;
if (!username) {
resp = {type: 'channel_private', channel: fwdChannel.title};
} else {
resp = {type: 'channel', username, channel: fwdChannel.title, id: message.forward_from_message_id}
}
} else if (fwdUser) {
// 投稿者转发自别人
resp = this.getUser(fwdUser);
resp.type = 'forward_user';
} else {
resp = this.getUser(user);
resp.type = 'user';
}
return resp;
};
/**
* 获取来源(如果是转发别人的信息): @xxx 一行
* @param {[type]} message [description]
* @return {[type]} [description]
*/
getFromText (message) {
let fwdInfo = this.getMessageFwdFromInfo(message);
let text = '';
if (fwdInfo.type == 'channel_private') {
text = this.get('sub_from_channel_private', fwdInfo)
} else if (fwdInfo.type == 'channel') {
text = this.get('sub_from_channel', fwdInfo)
} else if (fwdInfo.type == 'forward_user') {
text = this.get('sub_from', fwdInfo);
}
return text;
};
/**
* 得到 来源保留:保留/匿名 这一行
* @param {[type]} type [description]
* @return {[type]} [description]
*/
getFromReserve (type) {
let text = this.get('from_real');
if (type == this.vars.SUB_ANY) {
text = this.get('from_anonymous');
}
text = this.get('sub_from_reserve', {reserve: text});
return text;
};
/**
* 获取投稿人:@xxx 一行
* @param {[type]} message [description]
* @return {[type]} [description]
*/
getSubUser (message) {
return this.get('sub_people', this.getUser(message.from));
};
/**
* 获取一行: 更多帮助 [/command] 的文本
* @return {[type]} [description]
*/
getMoreHelp () {
return this.get('admin_morehelp', {command: '/pwshelp'});
};
/**
* 获取审稿群通用头部
* @param {[type]} message [description]
* @return {[type]} [description]
*/
getAdminCommonHeader (message) {
let text = this.get('sub_new') + "\n" + this.getSubUser(message);
// 是投稿人转发的信息,获取消息之来源
if (message.forward_date) {
text += "\n" + this.getFromText(message);
}
text += "\n" + this.getFromReserve(message.sub_type);
return text;
};
/**
* 机器人将稿件转发至审稿群后,询问管理员如何操作的文案
* 如 新投稿\n投稿人:xx\n...
* @param {String} type 操作类型
* @param {Object} message 稿件
* @return {[type]} [description]
*/
getAdminAction (message) {
let text = this.getAdminCommonHeader(message);
text += "\n\n" + this.getMoreHelp();
return text;
};
}
export default Lang;
================================================
FILE: src/utils/helper.js
================================================
import {config, blacklist, lang} from './../core';
import saveconfig from 'update-dotenv';
import msgControl from '../handler/msgControl';
/**
* 通常会用到的一些函式
* @type {[type]}
*/
export default {
/**
* 检查消息是否是命令
* @param {Object} message Message
* @return {Boolean}
*/
isCommand (message) {
return (message.entities
&& message.entities[0].type == 'bot_command') ? true : false;
},
/**
* 检查是否是私聊状态
* @param {Object} message message
* @return {Boolean}
*/
isPrivate (message) {
return (message.chat.type == 'private') ? true : false;
},
/**
* 消息来自管理员吗
* @param {[type]} message [description]
* @return {Boolean} [description]
*/
isAdmin (message) {
return (message.from.id == config.Admin) ? true : false;
},
/**
* 是本机器人吗
* @param {[type]} message [description]
* @return {Boolean} [description]
*/
isMe (message) {
let match = message.text.split('@');
return (match[1] && match[1] != config.BotUserName) ? false : true;
},
/**
* 检查此条消息是否是将机器人新加到群的提示
* @param {Object} message Message
* @return {Boolean}
*/
isNewJoin (message) {
return message.new_chat_member
&& message.new_chat_member.id == config.BotID ? true : false;
},
async updateConfig (params) {
for (let k in params) {
let v = params[k];
config[k] = v;
if (typeof v == 'number') {
params[k] = v.toString();
}
}
await saveconfig(params);
return true;
},
/**
* 获取10位的时间戳
* @return {number} 时间戳
*/
getTimestamp () {
return Math.floor(Date.now() / 1000);
},
/**
* 检查文本中是否含有URL
* @param {[type]} text [description]
* @return {Boolean} [description]
*/
hasUrl (text) {
let preg = /((http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?)/g;
return preg.test(text);
},
/**
* 检查现在是否需要静音
* @return {Boolean} [description]
*/
isMute () {
if (config.AutoMute) {
let hours = new Date().getHours();
if (hours > 23 || hours < 7) {
// 夜间静音
return true;
}
}
return false;
},
/**
* 传入用户消息,检查此人是否被封锁
* @param {[type]} message [description]
* @param {Boolean} showTips 回复被封锁消息
* @return {Boolean} [description]
*/
isBlock (message, showTips = false) {
if (blacklist.has({id: message.from.id})) {
if (showTips) {
msgControl.sendMessage(message.chat.id, lang.get('blacklist_ban_tips'));
}
return true;
}
return false;
},
/**
* 延迟执行
* @param {[type]} delay [description]
* @return {[type]} [description]
*/
sleep (delay) {
return new Promise(function(resolve) {
setTimeout(resolve, delay);
})
}
}
gitextract_gh6f860p/
├── .babelrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── ecosystem.config.js
├── env.example
├── package.json
├── readme.md
└── src/
├── core.js
├── handler/
│ ├── botCommand.js
│ ├── commandHandler.js
│ ├── msgControl.js
│ ├── msgHandler.js
│ └── queryHandler.js
├── lang/
│ ├── zh-CN.json
│ └── zh-TW.json
├── main.js
├── model/
│ ├── BlackList.js
│ ├── Db.js
│ ├── Message.js
│ └── Re.js
└── utils/
├── Lang.js
└── helper.js
SYMBOL INDEX (78 symbols across 9 files)
FILE: src/handler/msgControl.js
method subAsk (line 14) | subAsk (message) {
method editMessage (line 38) | editMessage (text, params = {}) {
method editCurrentMessage (line 51) | editCurrentMessage (text, message, params = {}) {
method sendCurrentMessage (line 65) | sendCurrentMessage (text, message, params = {}) {
method sendMessage (line 75) | sendMessage (chatId, text, params) {
method forwardMessage (line 88) | async forwardMessage (reply_to_message, type) {
method askAdmin (line 109) | async askAdmin ({reply_to_message_id, message}) {
method sendChannel (line 131) | async sendChannel (message, params) {
method receiveMessage (line 160) | async receiveMessage (message, receive, params = {}) {
method rejectMessage (line 188) | async rejectMessage (message, reject, reason) {
method replyMessage (line 214) | async replyMessage (message, comment, reMode = true) {
method replyMessageWithCommand (line 231) | async replyMessageWithCommand ({ msg, match, rep, repMsg }, command = '/...
method receive (line 251) | async receive (query) {
FILE: src/handler/msgHandler.js
method process (line 10) | process (message) {
FILE: src/handler/queryHandler.js
method process (line 10) | async process (query) {
method processSubmission (line 32) | async processSubmission (type, actionMsg) {
method isAdminReceiveAction (line 50) | isAdminReceiveAction (data) {
FILE: src/model/BlackList.js
class BlackList (line 4) | class BlackList extends Db
method unbanWithUserId (line 11) | unbanWithUserId (userId) {
method unbanWithMessage (line 24) | unbanWithMessage (repMsg) {
method banWithUserId (line 41) | banWithUserId (userId) {
method banWithMessage (line 54) | banWithMessage (repMsg) {
FILE: src/model/Db.js
class Db (line 8) | class Db
method db (line 14) | get db () {
method constructor (line 21) | constructor (table) {
method add (line 28) | add (data) {
method has (line 37) | has (data) {
method get (line 45) | get (data) {
method one (line 57) | one (data) {
method del (line 65) | del (data) {
method update (line 74) | update (condition, data) {
FILE: src/model/Message.js
class Message (line 3) | class Message extends Db
method process (line 13) | process (message, callback) {
method getReplytoMessageId (line 29) | getReplytoMessageId (message) {
method getCaption (line 38) | getCaption (message, params) {
method getOptions (line 58) | getOptions (message, caption, params) {
method withoutKeys (line 74) | withoutKeys (obj, keys) {
method processMediaMessage (line 86) | processMediaMessage (message) {
method getFwdMsgCondition (line 121) | getFwdMsgCondition (fwdMsg) {
method getMsgCondition (line 135) | getMsgCondition (message) {
method getMsgWithReply (line 149) | getMsgWithReply (fwdMsg) {
method optimize (line 165) | optimize () {
method pushMediaGroup (line 180) | pushMediaGroup (groupMsg) {
method push (line 188) | push (message) {
FILE: src/model/Re.js
class Re (line 5) | class Re extends Db
method end (line 12) | end (message) {
method endWithId (line 23) | endWithId (userId) {
method has (line 30) | has (userId) {
method start (line 39) | async start (message) {
FILE: src/utils/Lang.js
class Lang (line 4) | class Lang {
method constructor (line 9) | constructor (langName, vars) {
method language (line 17) | get language () {
method get (line 29) | get (key, context) {
method getAdminActionFinish (line 41) | getAdminActionFinish (message) {
method getAdminComment (line 56) | getAdminComment (message) {
method getAdminActionReject (line 60) | getAdminActionReject (message, reason) {
method getAdminReject (line 66) | getAdminReject (message) {
method getAdminreader (line 70) | getAdminreader (message) {
method getViaInfo (line 80) | getViaInfo (message) {
method getUser (line 97) | getUser (user) {
method getMessageFwdFromInfo (line 118) | getMessageFwdFromInfo (message) {
method getFromText (line 146) | getFromText (message) {
method getFromReserve (line 163) | getFromReserve (type) {
method getSubUser (line 176) | getSubUser (message) {
method getMoreHelp (line 183) | getMoreHelp () {
method getAdminCommonHeader (line 191) | getAdminCommonHeader (message) {
method getAdminAction (line 207) | getAdminAction (message) {
FILE: src/utils/helper.js
method isCommand (line 15) | isCommand (message) {
method isPrivate (line 24) | isPrivate (message) {
method isAdmin (line 32) | isAdmin (message) {
method isMe (line 40) | isMe (message) {
method isNewJoin (line 49) | isNewJoin (message) {
method updateConfig (line 53) | async updateConfig (params) {
method getTimestamp (line 68) | getTimestamp () {
method hasUrl (line 76) | hasUrl (text) {
method isMute (line 84) | isMute () {
method isBlock (line 100) | isBlock (message, showTips = false) {
method sleep (line 114) | sleep (delay) {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (66K chars).
[
{
"path": ".babelrc",
"chars": 184,
"preview": "{\n \"presets\": [\n [\"env\", {\n \"targets\": {\n \"node\": \"current\"\n }\n }],\n \"es2015\",\n"
},
{
"path": ".gitignore",
"chars": 206,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# MacOS\n.DS_Store\n\n# Database\ndb.json\n\n# Dependency di"
},
{
"path": ".travis.yml",
"chars": 198,
"preview": "language: node_js\nnode_js:\n - '12.16.1'\n - '12.1.0'\nbefore_install:\n - echo -e \"Token=$BOT_TOKEN\\nAdmin=$BOT_ADMIN\\nA"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2019 wepy\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "ecosystem.config.js",
"chars": 366,
"preview": "module.exports = {\n apps : [{\n name: 'pwsbot',\n script: 'npm run start',\n\n // Options reference: https://pm2.i"
},
{
"path": "env.example",
"chars": 440,
"preview": "# 机器人Token,必填,透过@Botfather机器人之父获取\nToken=\n# 管理员的userid(数字),必填(用于验证配置审稿群的权限),可透过@userinfo 机器人得到\nAdmin=\n# 投稿到的频道,必填,例 @abcd"
},
{
"path": "package.json",
"chars": 880,
"preview": "{\n \"name\": \"pwsbot\",\n \"version\": \"1.0.0\",\n \"description\": \"一个投稿机器人telegram\",\n \"scripts\": {\n \"start\": \"npm run bui"
},
{
"path": "readme.md",
"chars": 2496,
"preview": "# PWS - Telegram投稿机器人\n\n[](https://travis-"
},
{
"path": "src/core.js",
"chars": 1494,
"preview": "import TeleBot from 'node-telegram-bot-api';\nimport helper from './utils/helper';\nimport Lang from './utils/Lang';\nimpor"
},
{
"path": "src/handler/botCommand.js",
"chars": 5243,
"preview": "import {config, bot, helper, lang, subs, blacklist, re} from '../core';\nimport onText from './commandHandler';\nimport ms"
},
{
"path": "src/handler/commandHandler.js",
"chars": 1193,
"preview": "import { bot, vars, lang } from '../core';\nimport msgControl from './msgControl';\n\n/**\n * onText命令闭包函式\n * @param {Objec"
},
{
"path": "src/handler/msgControl.js",
"chars": 9656,
"preview": "import {config, bot, vars, lang, subs, helper, re} from '../core';\n\n/**\n * (主动)消息发送控制器\n * @type {Array}\n */\nexport defau"
},
{
"path": "src/handler/msgHandler.js",
"chars": 1248,
"preview": "import {config, bot, helper, lang, subs, blacklist, re} from '../core';\nimport msgControl from './msgControl';\n\n/**\n * 接"
},
{
"path": "src/handler/queryHandler.js",
"chars": 1605,
"preview": "import {config, bot, vars, lang, helper} from '../core';\nimport msgControl from './msgControl';\n\n/**\n * 点击actionMsg后会产生回"
},
{
"path": "src/lang/zh-CN.json",
"chars": 3391,
"preview": "{\n \"sub_new\": \"新投稿\",\n \"sub_people\": \"投稿人: [{{username}}](tg://user?id={{userid}})\",\n \"sub_from\": \"来源: [{{username}}]("
},
{
"path": "src/lang/zh-TW.json",
"chars": 3401,
"preview": "{\n \"sub_new\": \"新投稿\",\n \"sub_people\": \"投稿人: [{{username}}](tg://user?id={{userid}})\",\n \"sub_from\": \"來源: [{{username}}]("
},
{
"path": "src/main.js",
"chars": 655,
"preview": "import { bot } from '././core';\nimport queryHandler from './handler/queryHandler'\nimport botCommand from './handler/botC"
},
{
"path": "src/model/BlackList.js",
"chars": 1999,
"preview": "import { lang, vars, helper, subs } from '../core';\nimport Db from './Db';\n\nclass BlackList extends Db\n{\n /**\n * 取消封锁"
},
{
"path": "src/model/Db.js",
"chars": 1596,
"preview": "import low from 'lowdb';\nimport FileSync from 'lowdb/adapters/FileSync';\nconst adapter = new FileSync('db.json')\nconst d"
},
{
"path": "src/model/Message.js",
"chars": 5893,
"preview": "import { lang, vars, helper, re } from '../core';\nimport Db from './Db';\nclass Message extends Db\n{\n header = 0;\n /**\n"
},
{
"path": "src/model/Re.js",
"chars": 1398,
"preview": "import { lang, vars, helper, subs } from '../core';\nimport msgControl from '../handler/msgControl';\nimport Db from './Db"
},
{
"path": "src/utils/Lang.js",
"chars": 5869,
"preview": "/**\n * 用于翻译文案的工具\n */\nclass Lang {\n\n langName = null;\n lang = null;\n\n constructor (langName, vars) {\n this.langName"
},
{
"path": "src/utils/helper.js",
"chars": 2778,
"preview": "import {config, blacklist, lang} from './../core';\nimport saveconfig from 'update-dotenv';\nimport msgControl from '../ha"
}
]
About this extraction
This page contains the full source code of the axiref/telegram-pwsbot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (52.0 KB), approximately 17.9k tokens, and a symbol index with 78 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.