if redis:get('mattata:version') ~= configuration.version then
local success = dofile('migrate.lua')
print(success)
end
self.version = configuration.version
-- Make necessary database changes if the version has changed.
if not redis:get('mattata:version') or redis:get('mattata:version') ~= self.version then
redis:set('mattata:version', self.version)
end
self.last_update = self.last_update or 0 -- If there is no last update known, make it 0 so the bot doesn't encounter any problems when it tries to add the necessary increment.
self.last_backup = self.last_backup or os.date('%V')
-- Set a bunch of function aliases, for consistency & compatibility.
for i, v in pairs(api) do
mattata[i] = v
end
for i, v in pairs(tools) do
mattata[i] = v
end
for i, v in pairs(utils) do
if i ~= 'init' then
mattata[i] = v
end
end
function mattata:run(_, token)
-- mattata's main long-polling function which repeatedly checks the Telegram bot API for updates.
-- The objects received in the updates are then further processed through object-specific functions.
token = token or configuration.bot_token
assert(token, 'You need to enter your Telegram bot API token in configuration.lua, or pass it as the second argument when using the mattata:run() function!')
mattata.is_running = mattata.init(self) -- Initialise the bot.
utils.init(self, configuration)
while mattata.is_running do -- Perform the main loop whilst the bot is running.
local success = api.get_updates( -- Check the Telegram bot API for updates.
configuration.updates.timeout,
self.last_update + 1,
configuration.updates.limit,
json.encode(
{
'message',
'edited_message',
'inline_query',
'callback_query'
}
),
configuration.use_beta_endpoint or false
)
if success and success.result then
for _, v in ipairs(success.result) do
self.last_update = v.update_id
self.execution_time = socket.gettime()
if v.message or v.edited_message then
if v.edited_message then
v.message = v.edited_message
v.edited_message = nil
v.message.old_date = v.message.date
v.message.date = v.message.edit_date
v.message.edit_date = nil
v.message.is_edited = true
else
v.message.is_edited = false
end
if v.message.reply_to_message then
v.message.reply = v.message.reply_to_message -- Make the `update.message.reply_to_message`
-- object `update.message.reply` to make any future handling easier.
v.message.reply_to_message = nil -- Delete the old value by setting its value to nil.
end
mattata.on_message(self, v.message)
if configuration.debug then
print(
string.format(
'%s[36m[Update #%s] Message%s from %s to %s: %s%s[0m',
string.char(27),
v.update_id,
v.message.is_edited and ' edit' or '',
v.message.from.id,
v.message.chat.id,
v.message.text,
string.char(27)
)
)
end
elseif v.inline_query then
mattata.on_inline_query(self, v.inline_query)
if configuration.debug then
print(
string.format(
'%s[35m[Update #%s] Inline query from %s%s[0m',
string.char(27),
v.update_id,
v.inline_query.from.id,
string.char(27)
)
)
end
elseif v.callback_query then
if v.callback_query.message and v.callback_query.message.reply_to_message then
if message.text:match('^[/!#][%w_]+') and message.chat.type == 'supergroup' then
local command, input = message.text:lower():match('^[/!#]([%w_]+)(.*)$')
local all = redis:hgetall('chat:' .. message.chat.id .. ':aliases')
for alias, original in pairs(all) do
if command == alias then
message.text = '/' .. original .. input
message.is_alias = true
break
end
end
end
-- This is the main loop which iterates over configured plugins and runs the appropriate functions.
for _, plugin in ipairs(self.plugins) do
if plugin.is_beta_plugin and mattata.is_global_admin(message.from.id) then
self.is_allowed_beta_access = true
end
if not plugin.is_beta_plugin or (plugin.is_beta_plugin and self.is_allowed_beta_access) then
local commands = #plugin.commands or {}
for i = 1, commands do
if message.text:match(plugin.commands[i]) and mattata.is_plugin_allowed(plugin.name, self.is_user_blocklisted, configuration) and not self.is_command_done and not self.is_telegram and (not message.is_edited or mattata.is_global_admin(message.from.id)) then
if mattata.get_setting(message.chat.id, 'delete commands') and self.is_command and not redis:sismember('chat:' .. message.chat.id .. ':no_delete', tostring(plugin.name)) and not message.is_natural_language then
if not inline_query.query or inline_query.query:gsub('%s', '') == '' then
local offset = inline_query.offset and tonumber(inline_query.offset) or 0
local list = mattata.get_inline_list(self.info.username, offset)
if #list == 0 then
local title = 'No more results found!'
local description = 'There were no more inline features found. Use @' .. self.info.username .. ' <query> to search for more information about commands matching the given search query.'
local language = require('languages.' .. mattata.get_user_language(callback_query.from.id))
if message.chat.id and mattata.is_group(message) and mattata.get_setting(message.chat.id, 'force group language') then
language = require('languages.' .. (mattata.get_value(message.chat.id, 'group language') or 'en_gb'))
end
self.language = language
if redis:get('global_blocklist:' .. callback_query.from.id) and not callback_query.data:match('^join_captcha') and not mattata.is_global_admin(callback_query.from.id) then
return false, 'This user is globally blocklisted!'
elseif message and message.exists then
if message.reply and message.chat.type ~= 'channel' and callback_query.from.id ~= message.reply.from.id and not callback_query.data:match('^game:') and not callback_query.data:match('^report:') and not mattata.is_global_admin(callback_query.from.id) then
local output = 'Only ' .. message.reply.from.first_name .. ' can use this!'
-- A variant of mattata.send_message(), optimised for sending a message as a reply that forces a
-- reply back from the user.
function mattata.send_force_reply(message, text, parse_mode, disable_web_page_preview, token)
local success = api.send_message(
message,
text,
parse_mode,
disable_web_page_preview,
false,
message.message_id,
'{"force_reply":true,"selective":true}',
token
)
return success
end
function mattata.get_chat(chat_id, only_api, token)
local success = api.get_chat(chat_id, token)
if only_api then -- stops antispam using usernames stored in the database
return success
elseif success and success.result and success.result.type and success.result.type == 'private' then
mattata.process_user(success.result)
elseif success and success.result then
mattata.process_chat(success.result)
end
return success
end
function mattata.is_plugin_disabled(plugin, message, is_administrative)
if not plugin or not message then
return false
elseif type(message) == 'table' and message.chat.type == 'supergroup' and mattata.is_group_admin(message.chat.id, message.from.id) and mattata.get_setting(message.chat.id, 'enable plugins for admins') then
elseif message.from.language_code:len() == 2 or message.from.language_code == 'root' then -- not sure why but some english users were having `root` return as their language
message.from.language_code = 'en_us'
end
end
message.reply = message.reply and mattata.sort_message(message.reply) or nil
redis:expire('not_blocklisted:' .. message.from.id, 604800) -- Let the key last a week!
end
return false, jdat, jdat.error, jdat.code
elseif jdat.id then
return true, jdat, 'Success', 200
end
return false, jdat, 'Error!', jdat.code or 404
end
function mattata.process_afk(message) -- Checks if the message references an AFK user and tells the
-- person mentioning them that they are marked AFK. If a user speaks and is currently marked as AFK,
-- then the bot will announce their return along with how long they were gone for.
if message.from.username
and redis:hget('afk:' .. message.from.id, 'since')
and not mattata.is_plugin_disabled('afk', message)
and not message.text:match('^[/!#]afk')
and not message.text:lower():match('^i?\'?l?l? ?[bg][rt][bg].?$')
then
local since = os.time() - tonumber(redis:hget('afk:' .. message.from.id, 'since'))
redis:hdel('afk:' .. message.from.id, 'since')
redis:hdel('afk:' .. message.from.id, 'note')
local keys = redis:keys('afk:' .. message.from.id .. ':replied:*')
if #keys > 0 then
for _, key in pairs(keys) do
redis:del(key)
end
end
local output = message.from.first_name .. ' has returned, after being /AFK for ' .. mattata.format_time(since) .. '.'
mattata.send_message(message.chat.id, output)
elseif (message.text:match('@[%w_]+') -- If a user gets mentioned, check to see if they're AFK.
or message.reply) and not redis:get('afk:' .. message.from.id .. ':replied:' .. message.chat.id) then
local username = message.reply and message.reply.from.id or message.text:match('@([%w_]+)')
local success = mattata.get_user(username)
if not success or not success.result or not success.result.id then
return false
end
local exists = redis:hexists('afk:' .. success.result.id, 'since')
if success and success.result and exists then -- If all the checks are positive, the mentioned user is AFK, so we'll tell the person mentioning them that this is the case!
elseif text:match(name .. '.- resume my music') then
local myspotify = require('plugins.myspotify')
local success = myspotify.reauthorise_account(message.from.id, configuration)
local output = success and myspotify.play(message.from.id) or 'An error occured whilst trying to connect to your Spotify account, are you sure you\'ve connected me to it?'
mattata.send_message(message.chat.id, output)
end
message.is_natural_language = true
return message
end
function mattata.process_spam(message)
if message.forward_from then return false end
local msg_count = tonumber(
redis:get('antispam:' .. message.chat.id .. ':' .. message.from.id) -- Check to see if the user
-- has already sent 1 or more messages to the current chat, in the past 5 seconds.
)
or 1 -- If this is the first time the user has posted in the past 5 seconds, we'll make it 1 accordingly.
configuration.administration.global_antispam.ttl, -- set the TTL
msg_count + 1 -- Increase the current message count by 1.
)
if msg_count == configuration.administration.global_antispam.message_warning_amount -- If the user has sent x messages in the past y seconds, send them a warning.
-- and not mattata.is_global_admin(message.from.id)
and message.chat.type == 'private' then
-- Don't run the antispam plugin if the user is configured as a global admin in `configuration.lua`.
mattata.send_reply( -- Send a warning message to the user who is at risk of being blocklisted for sending
-- too many messages.
message,
string.format(
'Hey %s, please don\'t send that many messages, or you\'ll be forbidden to use me for 24 hours!',
message.from.username and '@' .. message.from.username or message.from.name
)
)
elseif msg_count == configuration.administration.global_antispam.message_blocklist_amount -- If the user has sent x messages in the past y seconds, blocklist them globally from
-- using the bot for 24 hours.
-- and not mattata.is_global_admin(message.from.id) -- Don't blocklist the user if they are configured as a global
if configuration.administration.global_antispam.blocklist_length ~= -1 and configuration.administration.global_antispam.blocklist_length ~= 'forever' then
'Sorry, %s, but you have been blocklisted from using me for the next 24 hours because you have been spamming!',
message.from.username and '@' .. message.from.username or message.from.name
)
)
end
return false
end
function mattata:process_language(message)
if message.from.language_code then
if not mattata.does_language_exist(message.from.language_code) then
if not redis:sismember('mattata:missing_languages', message.from.language_code) then -- If we haven't stored the missing language file, add it into the database.
if (message.text == '/start' or message.text == '/start@' .. self.info.username) and message.chat.type == 'private' then
mattata.send_message(
message.chat.id,
'It appears that I haven\'t got a translation in your language (' .. message.from.language_code .. ') yet. If you would like to voluntarily translate me into your language, please join <a href="https://t.me/mattataDev">my official development group</a>. Thanks!',
'html'
)
end
elseif redis:sismember('mattata:missing_languages', message.from.language_code) then
-- If the language file is found, yet it's recorded as missing in the database, it's probably
-- new, so it is deleted from the database to prevent confusion when processing this list!
if message.chat and message.chat.type ~= 'private' and not mattata.service_message(message) and not mattata.is_plugin_disabled('statistics', message) and not mattata.is_privacy_enabled(message.from.id) and not self.is_blocklisted then
if message.new_chat_members and mattata.get_setting(message.chat.id, 'use administration') and mattata.get_setting(message.chat.id, 'antibot') and not mattata.is_group_admin(message.chat.id, message.from.id) and not mattata.is_global_admin(message.from.id) then
local kicked = {}
local usernames = {}
for _, v in pairs(message.new_chat_members) do
if v.username and v.username:lower():match('bot$') and v.id ~= message.from.id and v.id ~= self.info.id and tostring(v.is_bot) == 'true' then
local success = mattata.kick_chat_member(message.chat.id, v.id)
if #kicked > 0 and #usernames > 0 and #kicked == #usernames then
local log_chat = mattata.get_log_chat(message.chat.id)
mattata.send_message(log_chat, string.format('<pre>%s [%s] has kicked %s from %s [%s] because anti-bot is enabled.</pre>', mattata.escape_html(self.info.first_name), self.info.id, table.concat(kicked, ', '), mattata.escape_html(message.chat.title), message.chat.id), 'html')
return mattata.send_message(message, string.format('Kicked %s because anti-bot is enabled.', table.concat(usernames, ', ')))
end
end
if message.chat.type == 'supergroup' and mattata.get_setting(message.chat.id, 'use administration') and mattata.get_setting(message.chat.id, 'word filter') and not mattata.is_group_admin(message.chat.id, message.from.id) and not mattata.is_global_admin(message.from.id) then
local words = redis:smembers('word_filter:' .. message.chat.id)
if words and #words > 0 then
for _, v in pairs(words) do
local text = message.text:lower()
if text:match('^' .. v:lower() .. '$') or text:match('^' .. v:lower() .. ' ') or text:match(' ' .. v:lower() .. ' ') or text:match(' ' .. v:lower() .. '$') then
local action = mattata.get_setting(message.chat.id, 'ban not kick') and mattata.ban_chat_member or mattata.kick_chat_member
local success = action(message.chat.id, message.from.id)
if success then
if mattata.get_setting(message.chat.id, 'log administrative actions') then
local log_chat = mattata.get_log_chat(message.chat.id)
mattata.send_message(log_chat, string.format('<pre>%s [%s] has kicked %s [%s] from %s [%s] for sending one or more prohibited words.</pre>', mattata.escape_html(self.info.first_name), self.info.id, mattata.escape_html(message.from.first_name), message.from.id, mattata.escape_html(message.chat.title), message.chat.id), 'html')
end
mattata.send_message(message.chat.id, string.format('Kicked %s for sending one or more prohibited words.', message.from.username and '@' .. message.from.username or message.from.first_name))
break_cycle = true
end
end
end
if break_cycle then return true end
end
end
if message.new_chat_members and message.chat.type ~= 'private' and mattata.get_setting(message.chat.id, 'use administration') and mattata.get_setting(message.chat.id, 'welcome message') and not mattata.get_setting(message.chat.id, 'require captcha') then
if message.new_chat_members[1].id == self.info.id or (message.new_chat_members[1].username and message.new_chat_members[1].username:match('[Bb][Oo][Tt]$')) then
return false -- we don't want to send a welcome message if it's us or another bot (we're going to assume normal users don't have a username ending in bot...)
end
local chat_member = mattata.get_chat_member(message.chat.id, message.new_chat_members[1].id)
if chat_member.result.can_send_messages == false then
- save.help = '/save - Stores the replied-to message in mattata\'s database - of which a randomly-selected, saved message from the said user can be retrieved using /quote.'
+ save.help = '/save - Stores the replied-to message in ' .. self.info.first_name .. '\'s database - of which a randomly-selected, saved message from the said user can be retrieved using /quote. Alias: /s.'
- quote.help = '/quote - Returns a randomly-selected, quoted message from the replied-to user. Quoted messages are stored when a user uses /save in reply to the said user\'s message(s).'
+ quote.help = '/quote - Returns a randomly-selected, quoted message from the replied-to user. Quoted messages are stored when a user uses /save in reply to the said user\'s message(s). Alias: /q.'
end
function quote.on_message(_, message, _, language)
if not message.reply then
local quotes = redis:keys('user:*:quotes')
if #quotes == 0 then
return false
end
local selected = quotes[math.random(#quotes)]
local user = selected:match('^user:(%d+):quotes$')
local all = redis:smembers('user:' .. user .. ':quotes')