diff --git a/README.md b/README.md index 8e9b72d..01e6ff9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Du benötigst **Lua 5.2+**, eine aktive **Redis-Instanz** und die folgenden **Lu * luasocket * luasec * multipart-post -* dkjson +* lua-cjson * lpeg * redis-lua * fakeredis diff --git a/miku/bot.lua b/miku/bot.lua index 90e6c1d..2d13ddf 100644 --- a/miku/bot.lua +++ b/miku/bot.lua @@ -93,6 +93,10 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec end msg = pre_process_msg(self, msg, config) + if is_service_msg(msg) then + msg = service_modify_msg(msg) + end + for _, plugin in ipairs(self.plugins) do match_plugins(self, msg, config, plugin) end diff --git a/miku/plugins/afk.lua b/miku/plugins/afk.lua index 8811d85..171e0b0 100644 --- a/miku/plugins/afk.lua +++ b/miku/plugins/afk.lua @@ -84,9 +84,9 @@ function afk:pre_process(msg, self) redis:hset(hash, 'afk_text', false) local afk_text = afk_text:gsub("%*","") local afk_text = afk_text:gsub("_","") - utilities.send_message(self, msg.chat.id, user_name..' ist wieder da! (war: *'..afk_text..'* für '..duration..')', true, nil, true) + utilities.send_reply(self, msg, user_name..' ist wieder da! (war: *'..afk_text..'* für '..duration..')', true, '{"hide_keyboard":true,"selective":true}') else - utilities.send_message(self, msg.chat.id, user_name..' ist wieder da! (war '..duration..' weg)') + utilities.send_reply(self, msg, user_name..' ist wieder da! (war '..duration..' weg)', false, '{"hide_keyboard":true,"selective":true}') end end @@ -103,8 +103,15 @@ function afk:action(msg) local chat_id = msg.chat.id local user_name = get_name(msg) local timestamp = msg.date + local uhash = 'user:'..msg.from.id + local show_afk_keyboard = redis:hget(uhash, 'afk_keyboard') + if show_afk_keyboard == 'true' then + keyboard = '{"keyboard":[[{"text":"Wieder da."}]], "one_time_keyboard":true, "selective":true, "resize_keyboard":true}' + else + keyboard = nil + end - utilities.send_reply(self, msg, afk:switch_afk(user_name, user_id, chat_id, timestamp, matches[2])) + utilities.send_reply(self, msg, afk:switch_afk(user_name, user_id, chat_id, timestamp, matches[2]), false, keyboard) end return afk \ No newline at end of file diff --git a/miku/plugins/echo.lua b/miku/plugins/echo.lua index 0c9abef..fb07e67 100644 --- a/miku/plugins/echo.lua +++ b/miku/plugins/echo.lua @@ -13,7 +13,7 @@ function echo:init(config) end function echo:inline_callback(inline_query, config, matches) - local text = matches[1] + local text = matches[1] local results = '[' -- enable custom markdown button diff --git a/miku/plugins/games.lua b/miku/plugins/games.lua index cf7952f..c421773 100644 --- a/miku/plugins/games.lua +++ b/miku/plugins/games.lua @@ -122,9 +122,17 @@ function games:send_game_data(game_id, self, msg) return end +function games:action(msg, config) + local game = utilities.input(msg.text) + if not game then + if msg.reply_to_message and msg.reply_to_message.text then + game = msg.reply_to_message.text + else + utilities.send_message(self, msg.chat.id, fun.doc, true, msg.message_id, true) + return + end + end -function games:action(msg, config, matches) - local game = URL.escape(matches[1]) local game_id = games:get_game_id(game) if not game_id then utilities.send_reply(self, msg, 'Spiel nicht gefunden!') diff --git a/miku/plugins/getfile.lua b/miku/plugins/getfile.lua index 77d0446..a82aa28 100644 --- a/miku/plugins/getfile.lua +++ b/miku/plugins/getfile.lua @@ -104,4 +104,4 @@ end function media_download:action(msg) end -return media_download +return media_download \ No newline at end of file diff --git a/miku/plugins/leave_group.lua b/miku/plugins/leave_group.lua deleted file mode 100644 index 532d6db..0000000 --- a/miku/plugins/leave_group.lua +++ /dev/null @@ -1,49 +0,0 @@ -local leave_group = {} - -leave_group.triggers = { - '/nil' -} - -local report_to_admin = true -- set to false to not be notified, when Bot leaves groups without you - -function leave_group:check_for_admin(msg, self, config) - local result = bindings.request(self, 'getChatMember', { - chat_id = msg.chat.id, - user_id = config.admin - } ) - if not result.ok then - print('Konnte nicht prüfen, ob Admin in Gruppe ist. Verlasse Gruppe.') - return false - end - if result.result.status ~= "member" and result.result.status ~= "administrator" and result.result.status ~= "creator" then - return false - else - return true - end -end - -function leave_group:pre_process(msg, self, config) - if msg.group_chat_created or msg.new_chat_member then - local admin_in_group = leave_group:check_for_admin(msg, self, config) - if not admin_in_group then - print('Admin ist nicht in der Gruppe, verlasse sie deshalb...') - utilities.send_reply(self, msg, 'Dieser Bot wurde in eine fremde Gruppe hinzugefügt. Dies wird gemeldet!\nThis bot was added to foreign group. This incident will be reported!') - local result = bindings.request(self, 'leaveChat', { - chat_id = msg.chat.id - } ) - local chat_name = msg.chat.title - local chat_id = msg.chat.id - local from = msg.from.name - local from_id = msg.from.id - if report_to_admin then - utilities.send_message(self, config.admin, '#WARNUNG: Bot wurde in fremde Gruppe hinzugefügt:\nGruppenname: '..chat_name..' ('..chat_id..')\nHinzugefügt von: '..from..' ('..from_id..')') - end - end - end - return msg -end - -function leave_group:action(msg) -end - -return leave_group diff --git a/miku/plugins/remind.lua b/miku/plugins/remind.lua index e691d83..01575f5 100644 --- a/miku/plugins/remind.lua +++ b/miku/plugins/remind.lua @@ -11,54 +11,56 @@ function remind:init(config) end function remind:action(msg) - -- Ensure there are arguments. If not, send doc. - local input = utilities.input(msg.text) - if not input then - utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) - return - end - -- Ensure first arg is a number. If not, send doc. - local duration = utilities.get_word(input, 1) - if not tonumber(duration) then - utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) - return - end - -- Duration must be between one minute and one year (approximately). - duration = tonumber(duration) - if duration < 1 then - duration = 1 - elseif duration > 526000 then - duration = 526000 - end - -- Ensure there is a second arg. - local message = utilities.input(input) - if not message then - utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) - return - end - -- Make a database entry for the group/user if one does not exist. - self.database.reminders[msg.chat.id_str] = self.database.reminders[msg.chat.id_str] or {} - -- Limit group reminders to 10 and private reminders to 50. - if msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[msg.chat.id_str]) > 9 then - utilities.send_reply(self, msg, 'Diese Gruppe hat schon zehn Erinnerungen!') - return - elseif msg.chat.type == 'private' and utilities.table_size(self.database.reminders[msg.chat.id_str]) > 49 then - utilities.send_reply(msg, 'Du hast schon 50 Erinnerungen!') - return - end - -- Put together the reminder with the expiration, message, and message to reply to. - local reminder = { - time = os.time() + duration * 60, - message = message - } - table.insert(self.database.reminders[msg.chat.id_str], reminder) - local output = 'Ich werde dich in ' .. duration - if duration == 1 then - output = output .. ' Minute erinnern!' - else - output = output .. ' Minuten erinnern!' - end - utilities.send_reply(self, msg, output) + -- Ensure there are arguments. If not, send doc. + local input = utilities.input(msg.text) + if not input then + utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) + return + end + + -- Ensure first arg is a number. If not, send doc. + local duration = utilities.get_word(input, 1) + if not tonumber(duration) then + utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) + return + end + + -- Duration must be between one minute and one day (approximately). + duration = tonumber(duration) + if duration < 1 then + duration = 1 + elseif duration > 1440 then + duration = 1440 + end + + -- Ensure there is a second arg. + local message = utilities.input(input) + if not message then + utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) + return + end + + -- Make a database entry for the group/user if one does not exist. + self.database.reminders[msg.chat.id_str] = self.database.reminders[msg.chat.id_str] or {} + -- Limit group reminders to 10 and private reminders to 50. + if msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[msg.chat.id_str]) > 9 then + utilities.send_reply(self, msg, 'Diese Gruppe hat schon zehn Erinnerungen!') + return + elseif msg.chat.type == 'private' and utilities.table_size(self.database.reminders[msg.chat.id_str]) > 49 then + utilities.send_reply(msg, 'Du hast schon 50 Erinnerungen!') + return + end + + -- Put together the reminder with the expiration, message, and message to reply to. + local timestamp = os.time() + duration * 60 + local reminder = { + time = timestamp, + message = message + } + table.insert(self.database.reminders[msg.chat.id_str], reminder) + local human_readable_time = convert_timestamp(timestamp, '%H:%M:%S') + local output = 'Ich werde dich um *'..human_readable_time..' Uhr* erinnern.' + utilities.send_reply(self, msg, output, true) end function remind:cron() diff --git a/miku/plugins/entergroup.lua b/miku/plugins/service_entergroup.lua similarity index 80% rename from miku/plugins/entergroup.lua rename to miku/plugins/service_entergroup.lua index 7423d82..2a05844 100644 --- a/miku/plugins/entergroup.lua +++ b/miku/plugins/service_entergroup.lua @@ -1,7 +1,8 @@ local entergroup = {} entergroup.triggers = { - '/nil' + '^//tgservice (new_chat_member)$', + '^//tgservice (left_chat_member)$' } function entergroup:chat_new_user(msg, self) @@ -38,17 +39,14 @@ function entergroup:chat_del_user(msg, self) utilities.send_reply(self, msg, text, true) end -function entergroup:pre_process(msg, self) - if msg.new_chat_member then +function entergroup:action(msg, config, matches) + if not is_service_msg(msg) then return end -- Bad attempt at trolling! + + if matches[1] == 'new_chat_member' then entergroup:chat_new_user(msg, self) - elseif msg.left_chat_member then + elseif matches[1] == 'left_chat_member'then entergroup:chat_del_user(msg, self) end - - return msg end -function entergroup:action(msg) -end - -return entergroup +return entergroup \ No newline at end of file diff --git a/miku/plugins/service_leave_group.lua b/miku/plugins/service_leave_group.lua new file mode 100644 index 0000000..f8ae1a7 --- /dev/null +++ b/miku/plugins/service_leave_group.lua @@ -0,0 +1,45 @@ +local leave_group = {} + +leave_group.triggers = { + '^//tgservice group_chat_created$', + '^//tgservice supergroup_chat_created$' +} + +local report_to_admin = true -- set to false to not be notified, when Bot leaves groups without you + +function leave_group:check_for_admin(msg, self, config) + local result = bindings.request(self, 'getChatMember', { + chat_id = msg.chat.id, + user_id = config.admin + } ) + if not result.ok then + print('Konnte nicht prfen, ob Admin in Gruppe ist! Verlasse sie sicherheitshalber...') + return false + end + if result.result.status ~= "member" and result.result.status ~= "administrator" and result.result.status ~= "creator" then + return false + else + return true + end +end + +function leave_group:action(msg, config) + if not is_service_msg(msg) then return end -- Bad attempt at trolling! + local admin_in_group = leave_group:check_for_admin(msg, self, config) + if not admin_in_group then + print('Admin ist nicht in der Gruppe, verlasse sie deshalb...') + utilities.send_reply(self, msg, 'Dieser Bot wurde in eine fremde Gruppe hinzugefgt. Dies wird gemeldet!\nThis bot was added to foreign group. This incident will be reported!') + local result = bindings.request(self, 'leaveChat', { + chat_id = msg.chat.id + } ) + local chat_name = msg.chat.title + local chat_id = msg.chat.id + local from = msg.from.name + local from_id = msg.from.id + if report_to_admin then + utilities.send_message(self, config.admin, '#WARNUNG: Bot wurde in fremde Gruppe hinzugefgt:\nGruppenname: '..chat_name..' ('..chat_id..')\nHinzugefgt von: '..from..' ('..from_id..')') + end + end +end + +return leave_group \ No newline at end of file diff --git a/miku/plugins/settings.lua b/miku/plugins/settings.lua new file mode 100644 index 0000000..b8a6f17 --- /dev/null +++ b/miku/plugins/settings.lua @@ -0,0 +1,54 @@ +local settings = {} + +settings.triggers = { + "^(⚙ [Ee]instellungen)$", + "^(/settings)$", + "^(💤 [Aa][Ff][Kk]%-[Kk]eyboard einschalten)", + "^(💤 [Aa][Ff][Kk]%-[Kk]eyboard ausschalten)", + "^(❌ [Ee]instellungen verstecken)" +} + +--[[ +[ + [ "Top Left", "Top Right" ], + [ "Bottom Left", "Bottom Right" ] +] +]] + +function settings:keyboard(user_id) + if redis:hget('user:'..user_id, 'afk_keyboard') == 'true' then + afk_button = '{"text":"💤 AFK-Keyboard ausschalten"}' + else + afk_button = '{"text":"💤 AFK-Keyboard einschalten"}' + end + local hide_settings_button = '{"text":"❌ Einstellungen verstecken"}' + + local settings_keyboard = '[['..afk_button..','..hide_settings_button..']]' + return settings_keyboard +end + +function settings:action(msg, config, matches) + if msg.chat.type ~= "private" then + return + end + + local hash = 'user:'..msg.from.id + + if matches[1] == '⚙ Einstellungen' or matches[1] == '/settings' then + utilities.send_reply(self, msg, 'Was möchtest du einstellen?', false, '{"keyboard":'..settings:keyboard(msg.from.id)..', "one_time_keyboard":true, "selective":true, "resize_keyboard":true}') + return + elseif matches[1] == '💤 AFK-Keyboard einschalten' then + redis:hset(hash, 'afk_keyboard', 'true') + utilities.send_reply(self, msg, 'Das AFK-Keyboard wurde erfolgreich *eingeschaltet*.', true, '{"keyboard":'..settings:keyboard(msg.from.id)..', "one_time_keyboard":true, "selective":true, "resize_keyboard":true}') + return + elseif matches[1] == '💤 AFK-Keyboard ausschalten' then + redis:hset(hash, 'afk_keyboard', 'false') + utilities.send_reply(self, msg, 'Das AFK-Keyboard wurde erfolgreich *ausgeschaltet*.', true, '{"keyboard":'..settings:keyboard(msg.from.id)..', "one_time_keyboard":true, "selective":true, "resize_keyboard":true}') + return + elseif matches[1] == '❌ Einstellungen verstecken' then + utilities.send_reply(self, msg, 'Um die Einstellungen wieder einzublenden, führe /settings aus.', true, '{"hide_keyboard":true}') + return + end +end + +return settings \ No newline at end of file diff --git a/miku/plugins/stats.lua b/miku/plugins/stats.lua index 3ba40bd..869ad76 100644 --- a/miku/plugins/stats.lua +++ b/miku/plugins/stats.lua @@ -80,9 +80,9 @@ end function stats:pre_process(msg, self) -- Ignore service msg - if msg.service then -- check how Bot API handles service msgs, will update this + if is_service_msg(msg) then print('Service message') - return + return msg end if msg.left_chat_member then diff --git a/miku/plugins/surrogate.lua b/miku/plugins/surrogate.lua index 2b52d94..e627c09 100644 --- a/miku/plugins/surrogate.lua +++ b/miku/plugins/surrogate.lua @@ -2,13 +2,17 @@ local surrogate = {} surrogate.triggers = { "^/s (%-%d+) +(.+)$", - "^/s (%d+) +(.+)$" + "^/s (%d+) +(.+)$", + "^/s (@[A-Za-z0-9-_-.-._.]+) +(.+)" } -function surrogate:action(msg) - -- Supergroups don't work!? +function surrogate:action(msg, config, matches) + if not is_sudo(msg, config) then + utilities.send_reply(self, msg, config.errors.sudo) + return + end utilities.send_message(self, matches[1], matches[2], true, nil, true) return end -return surrogate +return surrogate \ No newline at end of file diff --git a/miku/redis-old.lua b/miku/redis-old.lua deleted file mode 100644 index 6bdf76f..0000000 --- a/miku/redis-old.lua +++ /dev/null @@ -1,39 +0,0 @@ -local Redis = require 'redis' local FakeRedis = require 'fakeredis' -local params = { - host = os.getenv('REDIS_HOST') or '127.0.0.1', - port = tonumber(os.getenv('REDIS_PORT') or 6379) -} -local database = os.getenv('REDIS_DB') local password = -os.getenv('REDIS_PASSWORD') -- Overwrite HGETALL Redis.commands.hgetall -= Redis.command('hgetall', { - response = function(reply, command, ...) - local new_reply = { } - for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end - return new_reply - end -}) -local redis = nil -- Won't launch an error if fails local ok = -pcall(function() - redis = Redis.connect(params) end) if not ok then - local fake_func = function() - print('\27[31mCan\'t connect with Redis, install/configure -it!\27[39m') - end - fake_func() - fake = FakeRedis.new() - print('\27[31mRedis addr: '..params.host..'\27[39m') - print('\27[31mRedis port: '..params.port..'\27[39m') - redis = setmetatable({fakeredis=true}, { - __index = function(a, b) - if b ~= 'data' and fake[b] then - fake_func(b) - end - return fake[b] or fake_func - end }) else - if password then - redis:auth(password) - end - if database then - redis:select(database) - end end -return redis diff --git a/miku/utilities.lua b/miku/utilities.lua index 7f58285..09dd337 100644 --- a/miku/utilities.lua +++ b/miku/utilities.lua @@ -8,7 +8,7 @@ http = require('socket.http') https = require('ssl.https') socket = require('socket') URL = require('socket.url') -json = require('dkjson') +json = require('cjson') serpent = require("serpent") bindings = require('miku.bindings') redis = (loadfile "./miku/redis.lua")() @@ -254,12 +254,12 @@ function utilities.trim(str) return s end --- Retruns true if the string is empty +-- Returns true if the string is empty function string:isempty() return self == nil or self == '' end --- Retruns true if the string is blank +-- Returns true if the string is blank function string:isblank() self = self:trim() return self:isempty() @@ -631,6 +631,62 @@ function is_sudo(msg, config) return var end +function service_modify_msg(msg) + if msg.new_chat_member then + msg.text = '//tgservice new_chat_member' + msg.text_lower = msg.text + elseif msg.left_chat_member then + msg.text = '//tgservice left_chat_member' + msg.text_lower = msg.text + elseif msg.new_chat_title then + msg.text = '//tgservice new_chat_title' + msg.text_lower = msg.text + elseif msg.new_chat_photo then + msg.text = '//tgservice new_chat_photo' + msg.text_lower = msg.text + elseif msg.group_chat_created then + msg.text = '//tgservice group_chat_created' + msg.text_lower = msg.text + elseif msg.supergroup_chat_created then + msg.text = '//tgservice supergroup_chat_created' + msg.text_lower = msg.text + elseif msg.channel_chat_created then + msg.text = '//tgservice channel_chat_created' + msg.text_lower = msg.text + elseif msg.migrate_to_chat_id then + msg.text = '//tgservice migrate_to_chat_id' + msg.text_lower = msg.text + elseif msg.migrate_from_chat_id then + msg.text = '//tgservice migrate_from_chat_id' + msg.text_lower = msg.text + end + return msg +end + +function is_service_msg(msg) + local var = false + if msg.new_chat_member then + var = true + elseif msg.left_chat_member then + var = true + elseif msg.new_chat_title then + var = true + elseif msg.new_chat_photo then + var = true + elseif msg.group_chat_created then + var = true + elseif msg.supergroup_chat_created then + var = true + elseif msg.channel_chat_created then + var = true + elseif msg.migrate_to_chat_id then + var = true + elseif msg.migrate_from_chat_id then + var = true + end + return var +end + function post_petition(url, arguments, headers) local url, h = string.gsub(url, "http://", "") local url, hs = string.gsub(url, "https://", "") @@ -794,20 +850,62 @@ function get_http_header(url) return header, code end +-- checks with If-Modified-Since header, if url has been changed +-- URL and Last-Modified heder are required +function was_modified_since(url, last_modified) + local doer = http + local do_redir = true + if url:match('^https') then + doer = https + do_redir = false + end + local _, code, header = doer.request { + url = url, + method = "HEAD", + redirect = do_redir, + headers = { + ["If-Modified-Since"] = last_modified + } + } + if code == 304 then + return false, nil, code + else + if header["last-modified"] then + new_last_modified = header["last-modified"] + elseif header["Last-Modified"] then + new_last_modified = header["Last-Modified"] + end + return true, new_last_modified, code + end +end + -- only url is needed! function get_cached_file(url, file_name, receiver, chat_action, self) local hash = 'telegram:cache:sent_file' local cached_file_id = redis:hget(hash..':'..url, 'file_id') local cached_last_modified = redis:hget(hash..':'..url, 'last_modified') + if cached_last_modified then + was_modified, new_last_modified, code = was_modified_since(url, cached_last_modified) + if not was_modified then + print('File wasn\'t modified, skipping download...') + return cached_file_id, nil, true + else + if code ~= 200 then + redis:del(hash..':'..url) + return + end + print('File was modified, redownloading...') + if receiver and chat_action and self then + utilities.send_typing(self, receiver, chat_action) + end + file = download_to_file(url, file_name) + return file, new_last_modified, false + end + end + -- get last-modified and Content-Length header local header, code = get_http_header(url) - if code ~= 200 then - if cached_file_id then - redis:del(hash..':'..url) - end - return - end -- file size limit is 50 MB if header["Content-Length"] then @@ -839,14 +937,7 @@ function get_cached_file(url, file_name, receiver, chat_action, self) end if not nocache then - if last_modified == cached_last_modified then - print('File not modified and already cached') - nocache = true - file = cached_file_id - else - print('File cached, but modified or not already cached. (Re)downloading...') - file = download_to_file(url, file_name) - end + file = download_to_file(url, file_name) else print('No Last-Modified header!') file = download_to_file(url, file_name) @@ -861,11 +952,23 @@ function makeHumanTime(totalseconds) local minutes = minutes % 60 local hours = math.floor(totalseconds / 3600) if minutes == 00 and hours == 00 then - return seconds..' Sekunden' + if seconds == 1 then + return seconds..' Sekunde' + else + return seconds..' Sekunden' + end elseif hours == 00 and minutes ~= 00 then - return string.format("%02d:%02d", minutes, seconds)..' Minuten' + if minutes == 1 then + return string.format("%02d:%02d", minutes, seconds)..' Minute' + else + return string.format("%02d:%02d", minutes, seconds)..' Minuten' + end elseif hours ~= 00 then - return string.format("%02d:%02d:%02d", hours, minutes, seconds)..' Stunden' + if hours == 1 then + return string.format("%02d:%02d:%02d", hours, minutes, seconds)..' Stunde' + else + return string.format("%02d:%02d:%02d", hours, minutes, seconds)..' Stunden' + end end end