Commit c2fe7f37 by Иван Кубота

Implemented Abstractions:

Chat props: id<int> User props: id<int> chat<Chat> isAdmin<bool> - return true if user is admin of the group methods: send(text: string, [Choose]) - send PM to user. Really it is not useful due to bot can not initiate chat with user ban(duration: string) - ban user in group unban() - unban user. this would not readd user to the group (bot api does not support this action) capture(Event[]): <CaptureStop> - capture all next users messages in the messages group and begin to match them only against specified `Event list` Message props: raw - original data from API raw.message_id<int> user<User> chat<Chat> from<Message> - if message is a reply - it would contains previous message. TODO: rename it methods: send(text: string) - send new message to the messages channel reply(text: string) - send reply to the message remove() - remove message from channel Choose - abstraction for generating custom in-chat-buttons TODO: refactor code to standalone module. Example of rules: '''js // Admin can ban user by replying to his\her massage with `\ban`. Works only if user is not admin. Reply('/ban') .then(async function( message ){ if( (await message.user.isAdmin()) && !(await message.from.user.isAdmin())){ message.from.reply(L('ban', {...message.from.user})); message.from.user.ban(); } }) Answer('/say {{$kokoko}}') .then((message, match)=>{ message.send(match.kokoko) }), '''
parents
node_modules/*
.idea/*
tmp/*
^tmp/.keep
package-lock.json
\ No newline at end of file
const storage = require('./DB');
const Chat = function(chat) {
Object.assign(this, chat);
this.data = storage.load(this.id);
//chatDataStorage[this.id] = this.data = chatDataStorage[this.id] || {};
};
ChatFactory = function(cfg, cfg2) {
return new Chat(cfg, cfg2);
};
Chat.prototype = {
getUserData: function(user) {
if(!(user.id in this.data)){
this.data[ user.id ] = {};
}
return this.data[ user.id ];
},
setUserData: function(user) {
this.data[ user.id ] = user.data;
this.saveData();
},
saveData: function() {
storage.save(this.id, this.data);
console.log(JSON.stringify(this.data, null, 2))
}
};
module.exports = ChatFactory;
const waitingList = {
};
const Choose = function(list, callback) {
var data = list.map(function(item) {
if(!('url' in item)){
const randomHash = Math.random().toString(36).substr(2)+
Math.random().toString(36).substr(2)+
Math.random().toString(36).substr(2)+
Math.random().toString(36).substr(2)+
Math.random().toString(36).substr(2);
item.callback_data = randomHash;
waitingList[randomHash] = {item: item, fn: item.fn || callback};
}
const safeItem = {};
if('text' in item){
safeItem.text = item.text + '';
}
if('callback_data' in item){
safeItem.callback_data = item.callback_data + '';
}
if('url' in item){
safeItem.url = item.url + '';
}
return [safeItem];
});
return {
one_time_keyboard: true, reply_markup: JSON.stringify({inline_keyboard: data})
};
};
const resolver = function(msg) {
const cbData = msg.raw.data;
if(cbData in waitingList){
const item = waitingList[cbData];
if(item.fn){
item.fn.call(item.item, item.item, msg);
}
delete waitingList[cbData];
}
};
module.exports = {Choose, resolver};
\ No newline at end of file
var fs = require('fs');
var path = require('path');
const chatDataStorage = {};
const dir = path.join(__dirname, '../','tmp');
const getPath = function(name) {
return path.join( dir, name.toString(10) );
};
var entries = fs.readdirSync( dir );
for( var i = 0; i < entries.length; i++ ){
var entry = entries[i];
var fullPath = getPath(entry);
var stat = fs.statSync( fullPath );
if( stat.isFile() ){
try{
var result = JSON.parse( fs.readFileSync( fullPath ) );
chatDataStorage[entry] = result
}catch( e ){
console.log(e);
}
}
}
module.exports = {
save: function(id, data) {
try{
chatDataStorage[id] = data;
fs.writeFile( getPath( id ), JSON.stringify( data, null, 2 ), function(err, data) {
} )
}catch(e){
console.log(e);
}
},
load: function(id) {
if(!(id in chatDataStorage))
return {};
else
return chatDataStorage[id];
}
};
\ No newline at end of file
const Event = function(cfg, then) {
if(!cfg)
return;
this._match = cfg;
this.buildRegExp();
this._then = [];
if(then)
this.then(then);
};
const ReplyFactory = function(cfg, then) {
return new Reply(cfg, then);
};
Event.prototype = {
buildRegExp: function() {
const regTokenNames = this.regTokenNames = [];
this.matchRegExp = new RegExp(this._match.trim().replace(/\//g,'\\/').replace(/\{\{\$([^\}]*)\}\}/g, function(f,k){
regTokenNames.push(k);
return '(.*)'
}), 'i');
},
then: function(resolve, reject) {
this._then.push({resolve, reject});
return this;
},
match: function(msg) {
const matched = msg.raw.text.trim().match(this.matchRegExp)
if(!matched)
return false;
const out = {},
list = this.regTokenNames;
for(let i = 0, _i = list.length; i < _i; i++){
out[list[i]] = matched[i+1];
}
return out;
},
proceed: function(msg, match) {
for( let i = 0, _i = this._then.length; i < _i; i++ ){
const thenElement = this._then[ i ];
thenElement.resolve.call(msg, msg, match);
}
}
};
const Reply = function(cfg, then) {
Event.call(this, cfg, then);
};
Reply.prototype = new Event();
Reply.prototype.match = function(msg, then) {
if(!('reply_to_message' in msg.raw))
return false;
return Event.prototype.match.call(this, msg, then);
};
ReplyFactory.cls = Reply;
const AnswerFactory = function(cfg, then) {
return new Event(cfg, then);
};
module.exports = {Reply: ReplyFactory, Answer: AnswerFactory};
const User = require('./User'),
Chat = require('./Chat');
const Message = function(msg, bot) {
this.raw = msg;
this.bot = bot;
};
MessageFactory = function(msg, bot) {
return new Message(msg, bot);
};
Message.prototype = {
getUser: function() {
return this._user || (this._user = new User(this.raw.from, this.getChat(), this))
},
getChat: function() {
return this._chat || (this._chat = new Chat(this.raw.chat))
},
getFrom: function() {
if(!('reply_to_message' in this.raw)){
throw new Error('Message is not a reply:\n'+JSON.stringify(this.raw,null,2))
}
return this._from || (this._from = new Message(this.raw.reply_to_message, this.bot))
},
remove: function() {
return this.bot.deleteMessage(this.chat.id, this.raw.message_id);
},
send: function(text) {
this.bot.sendMessage(this.chat.id, text);
},
reply: function(text) {
this.bot.sendMessage(this.chat.id, text, {reply_to_message_id: this.raw.message_id});
}
};
Object.defineProperty(Message.prototype, 'user', {get: function() {
return this.getUser();
}});
Object.defineProperty(Message.prototype, 'chat', {get: function() {
return this.getChat();
}});
Object.defineProperty(Message.prototype, 'from', {get: function() {
return this.getFrom();
}});
module.exports = MessageFactory;
const ms = require('ms');
const User = function(userData, chat, msg) {
Object.assign(this, userData);
this.raw = userData;
this.chat = chat;
this.data = chat.getUserData(this);
this.msg = msg;
this.full_name = this.first_name+' '+this.last_name;
};
UserFactory = function(cfg, cfg2, msg) {
return new User(cfg, cfg2, msg);
};
User.prototype = {
isAdmin: async function() {
var admins = await this.msg.bot.getChatAdministrators(this.chat.id);
var admin = admins.filter((item)=>item.user.id === this.id);
return admin.length ? admin[0] : false;
},
ban: function(duration) {
this.msg.bot.kickChatMember(this.chat.id, this.id, {
until_date: Date.now() + ms(duration || '60d'),
});
},
unban: function() {
this.msg.bot.unbanChatMember(this.chat.id, this.id);
},
get: function(key) {
return this.data[key];
},
set: function(cfg) {
for(let key in cfg){
this.data[key] = cfg[key];
}
this.chat.setUserData(this);
},
send: function(text, opts) {
this.msg.bot.sendMessage(this.id, text, opts);
},
capture: function(arr) {
const capture = {
chat: this.chat.id,
user: this.id,
arr: arr
};
const chat = captures[this.chat.id] || (captures[this.chat.id] = {});
(chat[this.id] || (chat[this.id] = [])).push(capture);
return {
stop: function() {
chat[capture.user].splice(chat[capture.user].indexOf(capture), 1);
if(chat[capture.user].length === 0){
delete chat[ capture.user ];
}
if(Object.keys(chat).length === 0){
delete captures[capture.chat];
}
}
};
}
};
const captures = {};
UserFactory.captured = function(msg) {
if(msg.chat.id in captures){
const chat = captures[msg.chat.id];
if(msg.user.id in chat){
const capture = chat[msg.user.id];
const rules = capture[capture.length - 1].arr; // LIFO
for( let i = 0, _i = rules.length; i < _i; i++ ){
const rule = rules[ i ], match = rule.match(msg);
if(match){
rule.proceed(msg, match);
break;
}
}
return true;
}
}
return false;
};
module.exports = UserFactory;
const rules = require('./rules');
const Message = require('./Model/Message');
const TelegramBot = require('node-telegram-bot-api');
// replace the value below with the Telegram token you receive from @BotFather
const token = process.env.token;
// Create a bot that uses 'polling' to fetch new updates
const bot = new TelegramBot(token, {
polling: true
});
const captured = require('./Model/User').captured;
// Listen for any kind of message. There are different kinds of
// messages.
bot.on('message', (msg) => {
const wrappedMessage = new Message(msg, bot);
if(captured(wrappedMessage))
return;
for( let i = 0, _i = rules.length; i < _i; i++ ){
const rule = rules[ i ], match = rule.match(wrappedMessage);
if(match){
rule.proceed(wrappedMessage, match);
break;
}
}
});
const callbackResolver = require('./Model/Choose').resolver;
bot.on('callback_query', function (msg) {
const wrappedMessage = new Message(msg, bot);
callbackResolver(wrappedMessage);
});
\ No newline at end of file
module.exports = {
"ban": "Пользователь {{$first_name}} {{$last_name}} был забанен лопатой.",
"warn": "Пользователь {{$first_name}} {{$last_name}} получает {{$count}} предупреждение из {{$maxWarns}}, осталось {{$diff}} {{plural:$diff,предупреждение,предупреждения,предупреждений}}",
"unban": "Пользователя {{$full_name}} пришлось разбанить.",
"unbanGreeting": "Ты разбанены в {{$title}}",
"pidor": "{{$first_name}}, ты — {{$condition?пацак:чатланин}}, а эта планета {{$condition?чатланская:пацакская}}.\n\nСчитай до {{$count}}.",
"pidorDoNotCount1": "Считай, кому говорят",
"pidorDoNotCount2": "Транклюкирую!",
"pidorDoNotCount3": "Последний раз предупреждаю!",
"pidorDoNotCount4": "Пожизненный эцих без гвоздей",
"pidorDoNotCount5": "Пожизненный эцих с гвоздями",
"pidorDoigralsa": "{{$full_name}} уехал в эцихе",
"pidorKletka": "Здесь можно только в клетке выступать.",
"pidorKU": "Говори «Ку»!",
"pidorOK": "Всё, хорошо посчитал. Эцилоп вернётся ночью.",
"pidorCanNotCount": "Правильно считай.",
"pidorCanNotCount2": "Эцилоп, бей его. Считай с начала давай.",
"ban2": "{{$count}} {{plural:$count,рыба,рыбы,рыб}}",
"ku": "Говори «ку»",
"permission": "Не можно",
"_plural": "plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"
};
\ No newline at end of file
{
"name": "tg-bot-for-terkin",
"version": "0.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"localize.js": "^2.0.1",
"ms": "^2.1.2",
"node-telegram-bot-api": "^0.30.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Zibx",
"license": "MPL-2.0"
}
const {Reply, Answer} = require('./Model/Event'),
{Choose} = require('./Model/Choose'),
L = require('localize.js')('ru', require('./locale/locale.ru'));
const maxWarns = 3;
module.exports = [
Answer('/test {{$string}}', function(msg, match) {
var tmp = L.L.variableResolveFailed;
L.L.variableResolveFailed = function(varName) {
return '${'+varName+'}';
};
msg.reply(
'Template: '+L.voc[match.string]+'\n'+
'Result: '+L(match.string, {...msg.chat, ...msg.user})
);
L.L.variableResolveFailed = tmp;
}),
Answer('/help', function(message) {
message.send(
'List of commands:\n'+
module.exports.map((item)=>' '+(item instanceof Reply.cls?'>':' ')+item._match).join('\n')
);
}),
Reply('/warn')
.then((message)=>{
let warns = message.from.user.get('warns')|0;
warns++;
message.from.user.set({warns: warns});
if(warns>=maxWarns){
message.from.user.ban('60d');
message.reply(L('ban', {...message.from.user}))
}else{
message.reply(L('warn', {
maxWarns: maxWarns,
count: warns,
diff: maxWarns-warns,
...message.from.user
}))
}
}),
Reply('/ban')
.then((message)=>{
message.from.reply(L('ban', {...message.from.user}));
message.from.user.ban();
}),
Reply('/unban')
.then((message)=>{
message.from.user.unban();
//message.from.user.send(L('unbanGreeting', {...message.chat}));
message.reply(L('unban', {...message.from.user}))
}),
Reply('/reply')
.then( (msg)=>{
msg.send('Команда /reply пожрона');
msg.from.reply('Реплай на реплай');
}),
Reply('/test1')
.then((message)=>{
message.remove();
message.reply('Ti pidor')
}),
Answer('/say {{$kokoko}}')
.then((message, match)=>{
message.send(match.kokoko)
}),
Answer('/reply {{$text}}')
.then((msg, match)=>{
msg.send('Preparing to /reply');
msg.reply(match.text);
}),
Reply('/count {{$to}}', async function( message, match ){
if( (await message.user.isAdmin()) && !(await message.from.user.isAdmin())){
message.reply(L('permission'))
var usr = message.from.user;
var isPazak = ( usr.id % 2 ) === 0;
//var chatlanPlanet = isPazak;
var countTo = (match.to|0)||3;
message.from.send(L('pidor', {count: countTo, condition: isPazak, ...usr}));
/*message.from.user.full_name + ', ты — ' + ( isPazak ? 'пацак' : 'чатланин' ) + ', а это — ' + ( chatlanPlanet ? 'чатланская' : 'пацаковская' ) + ' планета.\n\n' +
'Считай до 5'
);*/
var last = 1;
var mistakes = 0;
var debil = 0;
var capture = message.from.user.capture( [
Answer('{{$any}}', (msg, match)=>{
var matched = match.any.match(/(\d+)/);
var matchedKU = match.any.match(/ку/i) !== null || match.any.match(/ыыы/i) !== null;
if(!matched && matchedKU){
// можно говорить "КУ" и "ЫЫЫ"
Math.random()>0.5 && msg.reply( L( 'pidorKletka' ) );
}else{
if( !matched ){
mistakes++;
if( mistakes <= 5 ){
msg.reply( L( 'pidorDoNotCount' + mistakes ) )
}else{
msg.reply( L( 'pidorDoigralsa' ) );
msg.user.ban();
capture.stop();
}
}else{
var num = parseInt( matched[ 0 ], 10 );
if( num === last ){
last++;
if(last % 3 === 0){
msg.reply( L( 'pidorKU' ) );
}
if(countTo === last - 1){
msg.reply( L( 'pidorOK' ) );
msg.reply( L( 'pidorKU' ) );
capture.stop();
}
}else{
debil++;
if(debil<3 || last === 1){
msg.reply( L( 'pidorCanNotCount' ) );
}else{
msg.reply( L( 'pidorCanNotCount2' ) );
last = 1;
}
}
}
}
})
] );
}
}),
Answer('/counter', async function( message ){
console.log( message.user.full_name + ' isAdmin:' + ( await message.user.isAdmin() ? 'true' : 'false' ) );
console.log( 1 );
for( let i = 0; i < 10; i++ )
setTimeout( () => message.send( i ), i * 1000 )
}),
Answer('/newPattern',function(message) {
message.user
.send('Which pattern?', Choose([
{ text: 'Read only'},
{ text: 'Text only'},
{ text: 'Timeout kick'},
], function(result) {
message.user.send('Selected: '+result.text);
}));
})
];
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment