Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None
diff --git a/src/core/router.lua b/src/core/router.lua
index 74533f3..2aca631 100644
--- a/src/core/router.lua
+++ b/src/core/router.lua
@@ -1,615 +1,634 @@
--[[
mattata v2.1 - Event Router
Dispatches Telegram updates through middleware pipeline to plugins.
Uses copas coroutines via telegram-bot-lua's async system for concurrent
update processing — each update runs in its own coroutine.
]]
local router = {}
local json = require('dkjson')
local copas = require('copas')
local config = require('src.core.config')
local logger = require('src.core.logger')
local middleware_pipeline = require('src.core.middleware')
local session = require('src.core.session')
local permissions = require('src.core.permissions')
local i18n = require('src.core.i18n')
local tools
local api, loader, ctx_base
-- Import middleware modules
local mw_blocklist = require('src.middleware.blocklist')
local mw_rate_limit = require('src.middleware.rate_limit')
local mw_user_tracker = require('src.middleware.user_tracker')
local mw_language = require('src.middleware.language')
local mw_federation = require('src.middleware.federation')
local mw_captcha = require('src.middleware.captcha')
local mw_stats = require('src.middleware.stats')
function router.init(api_ref, tools_ref, loader_ref, ctx_base_ref)
api = api_ref
tools = tools_ref
loader = loader_ref
ctx_base = ctx_base_ref
-- Register middleware in order
middleware_pipeline.use(mw_blocklist)
middleware_pipeline.use(mw_rate_limit)
middleware_pipeline.use(mw_federation)
middleware_pipeline.use(mw_captcha)
middleware_pipeline.use(mw_user_tracker)
middleware_pipeline.use(mw_language)
middleware_pipeline.use(mw_stats)
end
-- Build a fresh context for each update
-- Admin check is lazy — only resolved when ctx:check_admin() is called
local function build_ctx(message)
local ctx = {}
for k, v in pairs(ctx_base) do
ctx[k] = v
end
ctx.is_group = message.chat and message.chat.type ~= 'private'
ctx.is_supergroup = message.chat and message.chat.type == 'supergroup'
ctx.is_private = message.chat and message.chat.type == 'private'
ctx.is_global_admin = message.from and permissions.is_global_admin(message.from.id) or false
-- Lazy admin check: only makes API call when first accessed
-- Caches result for the lifetime of this context
local admin_resolved = false
local admin_value = false
ctx.is_admin = false -- default for non-admin reads
function ctx:check_admin()
if admin_resolved then
return admin_value
end
admin_resolved = true
if ctx.is_global_admin then
admin_value = true
elseif ctx.is_group and message.from then
admin_value = permissions.is_group_admin(api, message.chat.id, message.from.id)
end
ctx.is_admin = admin_value
return admin_value
end
-- For backward compat: admin plugins that check ctx.is_admin will still
-- need to call ctx:check_admin() first. The router does this for admin_only plugins.
ctx.is_mod = false
return ctx
end
-- Sort/normalise a message object (ported from v1 mattata.sort_message)
local function sort_message(message)
message.text = message.text or message.caption or ''
-- Normalise /command_arg to /command arg
message.text = message.text:gsub('^(/[%a]+)_', '%1 ')
-- Deep-link support
if message.text:match('^[/!#]start .-$') then
message.text = '/' .. message.text:match('^[/!#]start (.-)$')
end
-- Shorthand reply alias
if message.reply_to_message then
message.reply = message.reply_to_message
message.reply_to_message = nil
end
-- Normalise language code
if message.from and message.from.language_code then
local lc = message.from.language_code:lower():gsub('%-', '_')
if #lc == 2 and lc ~= 'en' then
lc = lc .. '_' .. lc
elseif #lc == 2 or lc == 'root' then
lc = 'en_us'
end
message.from.language_code = lc
end
-- Detect media
message.is_media = message.photo or message.video or message.audio or message.voice
or message.document or message.sticker or message.animation or message.video_note or false
-- Detect service messages
message.is_service_message = (message.new_chat_members or message.left_chat_member
or message.new_chat_title or message.new_chat_photo or message.pinned_message
or message.group_chat_created or message.supergroup_chat_created) and true or false
-- Entity-based text mentions -> ID substitution
if message.entities then
for _, entity in ipairs(message.entities) do
if entity.type == 'text_mention' and entity.user then
local name = message.text:sub(entity.offset + 1, entity.offset + entity.length)
message.text = message.text:gsub(name, tostring(entity.user.id), 1)
end
end
end
-- Process caption entities as entities
if message.caption_entities then
message.entities = message.caption_entities
message.caption_entities = nil
end
-- Sort reply recursively
if message.reply then
message.reply = sort_message(message.reply)
end
return message
end
-- Extract command from message text
local function extract_command(text, bot_username)
if not text then return nil, nil end
local cmd, args = text:match('^[/!#]([%w_]+)@?' .. (bot_username or '') .. '%s*(.*)')
if not cmd then
cmd, args = text:match('^[/!#]([%w_]+)%s*(.*)')
end
if cmd then
cmd = cmd:lower()
args = args ~= '' and args or nil
end
return cmd, args
end
-- Resolve aliases for a chat (with Redis caching)
local function resolve_alias(message, redis_mod)
if not message.text:match('^[/!#][%w_]+') then return message end
if not message.chat or message.chat.type == 'private' then return message end
local command, rest = message.text:lower():match('^[/!#]([%w_]+)(.*)')
if not command then return message end
-- Cache alias lookups with TTL instead of hgetall on every message
local cache_key = 'cache:aliases:' .. message.chat.id
local cached_aliases = redis_mod.get(cache_key)
local aliases
if cached_aliases then
local ok, decoded = pcall(json.decode, cached_aliases)
if ok and decoded then
aliases = decoded
end
end
if not aliases then
aliases = redis_mod.hgetall('chat:' .. message.chat.id .. ':aliases')
if type(aliases) == 'table' then
pcall(function()
redis_mod.setex(cache_key, 300, json.encode(aliases))
end)
end
end
if type(aliases) == 'table' then
for alias, original in pairs(aliases) do
if command == alias then
message.text = '/' .. original .. (rest or '')
message.is_alias = true
break
end
end
end
return message
end
-- Process action state (multi-step commands)
-- Fixed: save message_id before nil'ing message.reply
local function process_action(message, ctx)
if message.text and message.chat and message.reply
and message.reply.from and message.reply.from.id == api.info.id then
local reply_message_id = message.reply.message_id
local action = session.get_action(message.chat.id, reply_message_id)
if action then
message.text = action .. ' ' .. message.text
message.reply = nil
session.del_action(message.chat.id, reply_message_id)
end
end
return message
end
-- Build a lightweight context for non-message updates (no full middleware pipeline)
local function build_lightweight_ctx(chat, user)
local ctx = {}
for k, v in pairs(ctx_base) do
ctx[k] = v
end
if chat then
ctx.is_group = chat.type ~= 'private'
ctx.is_supergroup = chat.type == 'supergroup'
ctx.is_private = chat.type == 'private'
else
ctx.is_group = false
ctx.is_supergroup = false
ctx.is_private = true
end
ctx.is_global_admin = user and permissions.is_global_admin(user.id) or false
ctx.is_admin = false
ctx.is_mod = false
if user then
local lang_code = session.get_setting(user.id, 'language') or 'en_gb'
ctx.lang = i18n.get(lang_code)
end
return ctx
end
-- Handle a message update
local function on_message(message)
-- Validate
if not message or not message.from then return end
if message.date and message.date < os.time() - 10 then return end
-- Sort/normalise
message = sort_message(message)
message = process_action(message, ctx_base)
message = resolve_alias(message, ctx_base.redis)
-- Build context and run middleware
local ctx = build_ctx(message)
local should_continue
ctx, should_continue = middleware_pipeline.run(ctx, message)
if not should_continue then return end
-- Dispatch command to matching plugin
local cmd, args = extract_command(message.text, api.info.username)
if cmd then
local plugin = loader.get_by_command(cmd)
if plugin and plugin.on_message then
if not session.is_plugin_disabled(message.chat.id, plugin.name) or loader.is_permanent(plugin.name) then
-- Check permission requirements
if plugin.global_admin_only and not ctx.is_global_admin then
return
end
-- Resolve admin status only for admin_only plugins (lazy check)
if plugin.admin_only then
ctx:check_admin()
if not ctx.is_admin and not ctx.is_global_admin then
return api.send_message(message.chat.id, ctx.lang and ctx.lang.errors and ctx.lang.errors.admin or 'You need to be an admin to use this command.')
end
end
if plugin.group_only and ctx.is_private then
return api.send_message(message.chat.id, ctx.lang and ctx.lang.errors and ctx.lang.errors.supergroup or 'This command can only be used in groups.')
end
message.command = cmd
message.args = args
local ok, err = pcall(plugin.on_message, api, message, ctx)
if not ok then
logger.error('Plugin %s.on_message error: %s', plugin.name, tostring(err))
if config.log_chat() then
api.send_message(config.log_chat(), string.format(
'<pre>[%s] %s error:\n%s\nFrom: %s\nText: %s</pre>',
os.date('%X'), plugin.name,
tools.escape_html(tostring(err)),
message.from.id,
tools.escape_html(message.text or '')
), 'html')
end
end
end
end
end
-- Run passive handlers (on_new_message) for all non-disabled plugins
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_new_message and not session.is_plugin_disabled(message.chat.id, plugin.name) then
local ok, err = pcall(plugin.on_new_message, api, message, ctx)
if not ok then
logger.error('Plugin %s.on_new_message error: %s', plugin.name, tostring(err))
end
end
-- Handle member join events
if message.new_chat_members and plugin.on_member_join then
local ok, err = pcall(plugin.on_member_join, api, message, ctx)
if not ok then
logger.error('Plugin %s.on_member_join error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle callback query (routed through middleware for blocklist + rate limit)
local function on_callback_query(callback_query)
if not callback_query or not callback_query.from then return end
if not callback_query.data then return end
local message = callback_query.message
if not message then
message = {
chat = { id = callback_query.from.id, type = 'private' },
message_id = callback_query.inline_message_id,
from = callback_query.from
}
callback_query.is_inline = true
end
-- Parse plugin_name:data format
local plugin_name, cb_data = callback_query.data:match('^(.-):(.*)$')
if not plugin_name then return end
local plugin = loader.get_by_name(plugin_name)
if not plugin or not plugin.on_callback_query then return end
callback_query.data = cb_data
-- Build context and run basic middleware (blocklist + rate limit)
local ctx = build_ctx(message)
-- Check blocklist for callback user
if session.is_globally_blocklisted(callback_query.from.id) then
return
end
+ -- Rate limit callback queries (5 per 3 seconds per user)
+ local cb_rate_key = string.format('rate:cb:%s', callback_query.from.id)
+ local cb_count = ctx.redis.incr(cb_rate_key)
+ if cb_count == 1 then
+ ctx.redis.expire(cb_rate_key, 3)
+ end
+ if cb_count > 5 then
+ return api.answer_callback_query(callback_query.id, 'Slow down! You\'re pressing buttons too fast.')
+ end
+
-- Load language for callback user
local lang_code = session.get_setting(callback_query.from.id, 'language') or 'en_gb'
ctx.lang = i18n.get(lang_code)
local ok, err = pcall(plugin.on_callback_query, api, callback_query, message, ctx)
if not ok then
logger.error('Plugin %s.on_callback_query error: %s', plugin_name, tostring(err))
end
end
-- Handle inline query
local function on_inline_query(inline_query)
if not inline_query or not inline_query.from then return end
if session.is_globally_blocklisted(inline_query.from.id) then return end
+ -- Rate limit inline queries (3 per 2 seconds per user)
+ local iq_rate_key = string.format('rate:iq:%s', inline_query.from.id)
+ local redis_mod = ctx_base.redis
+ local iq_count = redis_mod.incr(iq_rate_key)
+ if iq_count == 1 then
+ redis_mod.expire(iq_rate_key, 2)
+ end
+ if iq_count > 3 then return end
+
local ctx = build_ctx({ from = inline_query.from, chat = { type = 'private' } })
local lang_code = session.get_setting(inline_query.from.id, 'language') or 'en_gb'
ctx.lang = i18n.get(lang_code)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_inline_query then
local ok, err = pcall(plugin.on_inline_query, api, inline_query, ctx)
if not ok then
logger.error('Plugin %s.on_inline_query error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle chat join request updates
local function on_chat_join_request(request)
if not request or not request.from or not request.chat then return end
if session.is_globally_blocklisted(request.from.id) then return end
local ctx = build_lightweight_ctx(request.chat, request.from)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_chat_join_request then
local ok, err = pcall(plugin.on_chat_join_request, api, request, ctx)
if not ok then
logger.error('Plugin %s.on_chat_join_request error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle chat member status changes
local function on_chat_member(update)
if not update or not update.chat then return end
local user = update.from or (update.new_chat_member and update.new_chat_member.user)
if not user then return end
local ctx = build_lightweight_ctx(update.chat, user)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_chat_member_update then
local ok, err = pcall(plugin.on_chat_member_update, api, update, ctx)
if not ok then
logger.error('Plugin %s.on_chat_member_update error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle bot's own chat member status changes (added/removed/promoted)
local function on_my_chat_member(update)
if not update or not update.chat then return end
local ctx = build_lightweight_ctx(update.chat, update.from)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_my_chat_member then
local ok, err = pcall(plugin.on_my_chat_member, api, update, ctx)
if not ok then
logger.error('Plugin %s.on_my_chat_member error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle message reaction changes
local function on_message_reaction(reaction)
if not reaction or not reaction.chat then return end
local user = reaction.user or reaction.actor_chat
if not user then return end
local ctx = build_lightweight_ctx(reaction.chat, user)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_reaction then
local ok, err = pcall(plugin.on_reaction, api, reaction, ctx)
if not ok then
logger.error('Plugin %s.on_reaction error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle poll state changes
local function on_poll(poll)
if not poll then return end
-- Polls have no chat context, use a minimal ctx
local ctx = build_lightweight_ctx(nil, nil)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_poll then
local ok, err = pcall(plugin.on_poll, api, poll, ctx)
if not ok then
logger.error('Plugin %s.on_poll error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle poll answer (user votes)
local function on_poll_answer(answer)
if not answer or not answer.user then return end
local ctx = build_lightweight_ctx(nil, answer.user)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_poll_answer then
local ok, err = pcall(plugin.on_poll_answer, api, answer, ctx)
if not ok then
logger.error('Plugin %s.on_poll_answer error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle chat boost events
local function on_chat_boost(boost)
if not boost or not boost.chat then return end
local ctx = build_lightweight_ctx(boost.chat, nil)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_chat_boost then
local ok, err = pcall(plugin.on_chat_boost, api, boost, ctx)
if not ok then
logger.error('Plugin %s.on_chat_boost error: %s', plugin.name, tostring(err))
end
end
end
end
-- Handle removed chat boost events
local function on_removed_chat_boost(boost)
if not boost or not boost.chat then return end
local ctx = build_lightweight_ctx(boost.chat, nil)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.on_removed_chat_boost then
local ok, err = pcall(plugin.on_removed_chat_boost, api, boost, ctx)
if not ok then
logger.error('Plugin %s.on_removed_chat_boost error: %s', plugin.name, tostring(err))
end
end
end
end
-- Concurrent polling loop using telegram-bot-lua's async system
function router.run()
local polling = config.polling()
-- Register telegram-bot-lua handler callbacks
-- api.process_update() dispatches to these inside per-update copas coroutines
api.on_message = function(msg)
local ok, err = pcall(on_message, msg)
if not ok then logger.error('on_message error: %s', tostring(err)) end
end
api.on_edited_message = function(msg)
msg.is_edited = true
local ok, err = pcall(on_message, msg)
if not ok then logger.error('on_edited_message error: %s', tostring(err)) end
end
api.on_callback_query = function(cb)
local ok, err = pcall(on_callback_query, cb)
if not ok then logger.error('on_callback_query error: %s', tostring(err)) end
end
api.on_inline_query = function(iq)
local ok, err = pcall(on_inline_query, iq)
if not ok then logger.error('on_inline_query error: %s', tostring(err)) end
end
api.on_chat_join_request = function(req)
local ok, err = pcall(on_chat_join_request, req)
if not ok then logger.error('on_chat_join_request error: %s', tostring(err)) end
end
api.on_chat_member = function(update)
local ok, err = pcall(on_chat_member, update)
if not ok then logger.error('on_chat_member error: %s', tostring(err)) end
end
api.on_my_chat_member = function(update)
local ok, err = pcall(on_my_chat_member, update)
if not ok then logger.error('on_my_chat_member error: %s', tostring(err)) end
end
api.on_message_reaction = function(reaction)
local ok, err = pcall(on_message_reaction, reaction)
if not ok then logger.error('on_message_reaction error: %s', tostring(err)) end
end
api.on_poll = function(poll)
local ok, err = pcall(on_poll, poll)
if not ok then logger.error('on_poll error: %s', tostring(err)) end
end
api.on_poll_answer = function(answer)
local ok, err = pcall(on_poll_answer, answer)
if not ok then logger.error('on_poll_answer error: %s', tostring(err)) end
end
api.on_chat_boost = function(boost)
local ok, err = pcall(on_chat_boost, boost)
if not ok then logger.error('on_chat_boost error: %s', tostring(err)) end
end
api.on_removed_chat_boost = function(boost)
local ok, err = pcall(on_removed_chat_boost, boost)
if not ok then logger.error('on_removed_chat_boost error: %s', tostring(err)) end
end
-- Cron: copas background thread, runs every 60s
copas.addthread(function()
while true do
copas.pause(60)
for _, plugin in ipairs(loader.get_plugins()) do
if plugin.cron then
copas.addthread(function()
local ok, err = pcall(plugin.cron, api, ctx_base)
if not ok then
logger.error('Plugin %s cron error: %s', plugin.name, tostring(err))
end
end)
end
end
end
end)
-- Stats flush: copas background thread, runs every 300s
copas.addthread(function()
while true do
copas.pause(300)
local ok, err = pcall(mw_stats.flush, ctx_base.db, ctx_base.redis)
if not ok then logger.error('Stats flush error: %s', tostring(err)) end
end
end)
-- Start concurrent polling loop
-- api.run() -> api.async.run() which:
-- 1. Swaps api.request to copas-based api.async.request
-- 2. Spawns polling coroutine calling get_updates in a loop
-- 3. For each update, spawns NEW coroutine -> api.process_update -> handlers above
-- 4. Calls copas.loop()
api.run({
timeout = polling.timeout,
limit = polling.limit,
allowed_updates = {
'message', 'edited_message', 'callback_query', 'inline_query',
'chat_join_request', 'chat_member', 'my_chat_member',
'message_reaction', 'message_reaction_count',
'poll', 'poll_answer',
'chat_boost', 'removed_chat_boost'
}
})
end
return router
diff --git a/src/plugins/admin/join_captcha.lua b/src/plugins/admin/join_captcha.lua
index afbef6b..5ffd5af 100644
--- a/src/plugins/admin/join_captcha.lua
+++ b/src/plugins/admin/join_captcha.lua
@@ -1,166 +1,169 @@
--[[
mattata v2.0 - Join Captcha Plugin
Handles captcha verification for new members joining the group.
]]
local plugin = {}
plugin.name = 'join_captcha'
plugin.category = 'admin'
plugin.description = 'Captcha challenge for new members'
plugin.commands = {}
plugin.help = ''
plugin.group_only = true
plugin.admin_only = false
local json = require('dkjson')
-- Generate a simple math captcha
local function generate_captcha()
math.randomseed(os.time())
local a = math.random(1, 20)
local b = math.random(1, 20)
local operators = { '+', '-' }
local op = operators[math.random(1, 2)]
local answer
if op == '+' then
answer = a + b
else
-- Ensure non-negative result
if a < b then a, b = b, a end
answer = a - b
end
return string.format('%d %s %d', a, op, b), tostring(answer)
end
-- Generate wrong answers for the keyboard
local function generate_options(correct_answer)
local options = { correct_answer }
local correct_num = tonumber(correct_answer)
while #options < 4 do
local wrong = correct_num + math.random(-5, 5)
if wrong ~= correct_num and wrong >= 0 then
local str = tostring(wrong)
local duplicate = false
for _, v in ipairs(options) do
if v == str then duplicate = true; break end
end
if not duplicate then
table.insert(options, str)
end
end
end
-- Shuffle
for i = #options, 2, -1 do
local j = math.random(1, i)
options[i], options[j] = options[j], options[i]
end
return options
end
function plugin.on_member_join(api, message, ctx)
if not ctx.is_group then return end
-- Check if captcha is enabled
local enabled = ctx.db.execute(
"SELECT value FROM chat_settings WHERE chat_id = $1 AND key = 'captcha_enabled'",
{ message.chat.id }
)
if not enabled or #enabled == 0 or enabled[1].value ~= 'true' then
return
end
if not require('src.core.permissions').can_restrict(api, message.chat.id) then return end
local timeout_result = ctx.db.execute(
"SELECT value FROM chat_settings WHERE chat_id = $1 AND key = 'captcha_timeout'",
{ message.chat.id }
)
local timeout = (timeout_result and #timeout_result > 0) and tonumber(timeout_result[1].value) or 300
for _, new_member in ipairs(message.new_chat_members) do
if new_member.is_bot then goto continue end
-- Restrict the new member
api.restrict_chat_member(message.chat.id, new_member.id, os.time() + timeout, {
can_send_messages = false,
can_send_media_messages = false,
can_send_other_messages = false,
can_add_web_page_previews = false
})
-- Generate captcha
local question, answer = generate_captcha()
local options = generate_options(answer)
-- Build keyboard
local keyboard = { inline_keyboard = { {} } }
for _, opt in ipairs(options) do
table.insert(keyboard.inline_keyboard[1], {
text = opt,
callback_data = string.format('join_captcha:%s:%s:%s', message.chat.id, new_member.id, opt)
})
end
local tools = require('telegram-bot-lua.tools')
local text = string.format(
'Welcome, <a href="tg://user?id=%d">%s</a>! Please solve this to verify you\'re human:\n\n<b>What is %s?</b>\n\nYou have %d seconds.',
new_member.id,
tools.escape_html(new_member.first_name),
question,
timeout
)
- local sent = api.send_message(message.chat.id, text, 'html', false, false, nil, json.encode(keyboard))
+ local sent = api.send_message(message.chat.id, text, {
+ parse_mode = 'html',
+ reply_markup = json.encode(keyboard)
+ })
-- Store captcha state
if sent and sent.result then
ctx.session.set_captcha(message.chat.id, new_member.id, answer, sent.result.message_id, timeout)
end
::continue::
end
end
function plugin.on_callback_query(api, callback_query, message, ctx)
local data = callback_query.data
if not data then return end
local chat_id, user_id, selected = data:match('^(%-?%d+):(%d+):(.+)$')
if not chat_id then return end
chat_id = tonumber(chat_id)
user_id = tonumber(user_id)
-- Only the joining user can answer
if callback_query.from.id ~= user_id then
return api.answer_callback_query(callback_query.id, 'This captcha is not for you.')
end
local captcha = ctx.session.get_captcha(chat_id, user_id)
if not captcha then
return api.answer_callback_query(callback_query.id, 'This captcha has expired.')
end
if selected == captcha.text then
-- Correct answer - unrestrict user
api.restrict_chat_member(chat_id, user_id, 0, {
can_send_messages = true,
can_send_media_messages = true,
can_send_other_messages = true,
can_add_web_page_previews = true
})
ctx.session.clear_captcha(chat_id, user_id)
local tools = require('telegram-bot-lua.tools')
api.edit_message_text(message.chat.id, message.message_id, string.format(
'<a href="tg://user?id=%d">%s</a> has been verified. Welcome!',
user_id, tools.escape_html(callback_query.from.first_name)
), 'html')
api.answer_callback_query(callback_query.id, 'Correct! Welcome to the group.')
else
-- Wrong answer
api.answer_callback_query(callback_query.id, 'Wrong answer. Try again!')
end
end
return plugin
diff --git a/src/plugins/admin/wordfilter.lua b/src/plugins/admin/wordfilter.lua
index 28b9e32..55af06c 100644
--- a/src/plugins/admin/wordfilter.lua
+++ b/src/plugins/admin/wordfilter.lua
@@ -1,94 +1,94 @@
--[[
mattata v2.0 - Word Filter Plugin
]]
local plugin = {}
plugin.name = 'wordfilter'
plugin.category = 'admin'
plugin.description = 'Toggle word filter and process filtered messages'
plugin.commands = { 'wordfilter' }
plugin.help = '/wordfilter <on|off> - Toggle word filtering. Filtered words are managed with /filter and /unfilter.'
plugin.group_only = true
plugin.admin_only = true
function plugin.on_message(api, message, ctx)
if not message.args then
local enabled = ctx.db.call('sp_get_chat_setting', { message.chat.id, 'wordfilter_enabled' })
local status = (enabled and #enabled > 0 and enabled[1].value == 'true') and 'enabled' or 'disabled'
return api.send_message(message.chat.id, string.format(
'Word filter is currently <b>%s</b>.\nUsage: /wordfilter <on|off>', status
- ), 'html')
+ ), { parse_mode = 'html' })
end
local arg = message.args:lower()
if arg == 'on' or arg == 'enable' then
ctx.db.call('sp_upsert_chat_setting', { message.chat.id, 'wordfilter_enabled', 'true' })
require('src.core.session').invalidate_setting(message.chat.id, 'wordfilter_enabled')
return api.send_message(message.chat.id, 'Word filter has been enabled.')
elseif arg == 'off' or arg == 'disable' then
ctx.db.call('sp_upsert_chat_setting', { message.chat.id, 'wordfilter_enabled', 'false' })
require('src.core.session').invalidate_setting(message.chat.id, 'wordfilter_enabled')
return api.send_message(message.chat.id, 'Word filter has been disabled.')
else
return api.send_message(message.chat.id, 'Usage: /wordfilter <on|off>')
end
end
function plugin.on_new_message(api, message, ctx)
if not ctx.is_group or not message.text or message.text == '' then return end
if ctx.is_admin or ctx.is_global_admin then return end
if not require('src.core.permissions').can_delete(api, message.chat.id) then return end
-- check if wordfilter is enabled (cached)
local session = require('src.core.session')
local enabled = session.get_cached_setting(message.chat.id, 'wordfilter_enabled', function()
local result = ctx.db.call('sp_get_chat_setting', { message.chat.id, 'wordfilter_enabled' })
if result and #result > 0 then return result[1].value end
return nil
end, 300)
if enabled ~= 'true' then
return
end
-- get filters for this chat (cached)
local filters = session.get_cached_list(message.chat.id, 'filters', function()
return ctx.db.call('sp_get_filters', { message.chat.id })
end, 300)
if not filters or #filters == 0 then return end
local text = message.text:lower()
for _, f in ipairs(filters) do
local match = pcall(function()
return text:match(f.pattern:lower())
end)
if match and text:match(f.pattern:lower()) then
-- execute action
if f.action == 'delete' then
api.delete_message(message.chat.id, message.message_id)
elseif f.action == 'warn' then
api.delete_message(message.chat.id, message.message_id)
local hash = string.format('chat:%s:%s', message.chat.id, message.from.id)
ctx.redis.hincrby(hash, 'warnings', 1)
api.send_message(message.chat.id, string.format(
'<a href="tg://user?id=%d">%s</a> has been warned for using a filtered word.',
message.from.id, require('telegram-bot-lua.tools').escape_html(message.from.first_name)
- ), 'html')
+ ), { parse_mode = 'html' })
elseif f.action == 'ban' then
api.delete_message(message.chat.id, message.message_id)
api.ban_chat_member(message.chat.id, message.from.id)
elseif f.action == 'kick' then
api.delete_message(message.chat.id, message.message_id)
api.ban_chat_member(message.chat.id, message.from.id)
api.unban_chat_member(message.chat.id, message.from.id)
elseif f.action == 'mute' then
api.delete_message(message.chat.id, message.message_id)
api.restrict_chat_member(message.chat.id, message.from.id, os.time() + 3600, {
can_send_messages = false
})
end
return
end
end
end
return plugin
diff --git a/src/plugins/media/gif.lua b/src/plugins/media/gif.lua
index e2eccee..c943906 100644
--- a/src/plugins/media/gif.lua
+++ b/src/plugins/media/gif.lua
@@ -1,63 +1,63 @@
--[[
mattata v2.0 - GIF Plugin
Searches for GIFs using the Tenor API and sends them as animations.
]]
local plugin = {}
plugin.name = 'gif'
plugin.category = 'media'
plugin.description = 'Search for GIFs using Tenor'
plugin.commands = { 'gif', 'tenor' }
plugin.help = '/gif <query> - Search for a GIF and send it.'
local TENOR_KEY = 'AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ'
function plugin.on_message(api, message, ctx)
local https = require('ssl.https')
local json = require('dkjson')
local url = require('socket.url')
local ltn12 = require('ltn12')
if not message.args or message.args == '' then
- return api.send_message(message.chat.id, 'Please specify a search query, e.g. <code>/gif funny cats</code>.', 'html')
+ return api.send_message(message.chat.id, 'Please specify a search query, e.g. <code>/gif funny cats</code>.', { parse_mode = 'html' })
end
local query = url.escape(message.args)
local api_url = string.format(
'https://tenor.googleapis.com/v2/search?q=%s&key=%s&limit=1&media_filter=gif',
query, TENOR_KEY
)
local response_body = {}
local res, code = https.request({
url = api_url,
method = 'GET',
sink = ltn12.sink.table(response_body),
headers = {
['Accept'] = 'application/json'
}
})
if not res or code ~= 200 then
return api.send_message(message.chat.id, 'Failed to search Tenor. Please try again later.')
end
local body = table.concat(response_body)
local data, _ = json.decode(body)
if not data or not data.results or #data.results == 0 then
return api.send_message(message.chat.id, 'No GIFs found for that query.')
end
local result = data.results[1]
local gif_url = result.media_formats
and result.media_formats.gif
and result.media_formats.gif.url
if not gif_url then
return api.send_message(message.chat.id, 'Failed to retrieve the GIF URL.')
end
return api.send_animation(message.chat.id, gif_url)
end
return plugin
diff --git a/src/plugins/utility/about.lua b/src/plugins/utility/about.lua
index 2fec74e..1808b87 100644
--- a/src/plugins/utility/about.lua
+++ b/src/plugins/utility/about.lua
@@ -1,22 +1,22 @@
--[[
mattata v2.0 - About Plugin
]]
local plugin = {}
plugin.name = 'about'
plugin.category = 'utility'
plugin.description = 'View information about the bot'
plugin.commands = { 'about' }
plugin.help = '/about - View information about the bot.'
plugin.permanent = true
function plugin.on_message(api, message, ctx)
local config = require('src.core.config')
local output = string.format(
'Created by <a href="tg://user?id=221714512">Matt</a>. Powered by <code>mattata v%s</code>. Source code available <a href="https://github.com/wrxck/mattata">on GitHub</a>.',
config.VERSION
)
- return api.send_message(message.chat.id, output, 'html')
+ return api.send_message(message.chat.id, output, { parse_mode = 'html' })
end
return plugin
diff --git a/src/plugins/utility/help.lua b/src/plugins/utility/help.lua
index 31c64c4..6f1843e 100644
--- a/src/plugins/utility/help.lua
+++ b/src/plugins/utility/help.lua
@@ -1,161 +1,180 @@
--[[
mattata v2.0 - Help Plugin
Displays help menus with inline keyboard navigation.
]]
local plugin = {}
plugin.name = 'help'
plugin.category = 'utility'
plugin.description = 'View bot help and command list'
plugin.commands = { 'help', 'start' }
plugin.help = '/help [command] - View help menu or get usage info for a specific command.'
plugin.permanent = true
local PER_PAGE = 10
local function get_page(items, page)
local start_idx = (page - 1) * PER_PAGE + 1
local end_idx = math.min(start_idx + PER_PAGE - 1, #items)
local result = {}
for i = start_idx, end_idx do
table.insert(result, items[i])
end
return result, math.ceil(#items / PER_PAGE)
end
local function format_help_list(help_items)
local lines = {}
for _, item in ipairs(help_items) do
local cmd = item.commands[1] and ('/' .. item.commands[1]) or ''
local desc = item.description or ''
table.insert(lines, string.format('%s %s - <em>%s</em>', '\xe2\x80\xa2', cmd, desc))
end
return table.concat(lines, '\n')
end
function plugin.on_message(api, message, ctx)
local tools = require('telegram-bot-lua.tools')
local loader = require('src.core.loader')
-- If argument given, show help for specific command
if message.args and message.args ~= '' then
local input = message.args:match('^/?(%w+)$')
if input then
local target = loader.get_by_command(input:lower())
if target and target.help then
return api.send_message(message.chat.id, 'Usage:\n' .. target.help .. '\n\nTo see all commands, send /help.')
end
return api.send_message(message.chat.id, 'No plugin found matching that command. Send /help to see all available commands.')
end
end
-- Show main help menu
local name = tools.escape_html(message.from.first_name)
local output = string.format(
'Hey %s! I\'m <b>%s</b>, a feature-rich Telegram bot.\n\nUse the buttons below to navigate my commands, or type <code>/help &lt;command&gt;</code> for details on a specific command.',
name, tools.escape_html(api.info.first_name)
)
local keyboard = api.inline_keyboard():row(
api.row():callback_data_button('Commands', 'help:cmds:1')
:callback_data_button('Admin Help', 'help:acmds:1')
):row(
api.row():callback_data_button('Links', 'help:links')
:callback_data_button('Settings', 'help:settings')
)
- return api.send_message(message.chat.id, output, 'html', true, false, nil, keyboard)
+ return api.send_message(message.chat.id, output, {
+ parse_mode = 'html',
+ disable_web_page_preview = true,
+ reply_markup = keyboard
+ })
end
function plugin.on_callback_query(api, callback_query, message, ctx)
local tools = require('telegram-bot-lua.tools')
local loader = require('src.core.loader')
local data = callback_query.data
if data:match('^cmds:%d+$') then
local page = tonumber(data:match('^cmds:(%d+)$'))
local all_help = loader.get_help(nil)
-- Filter non-admin
local items = {}
for _, h in ipairs(all_help) do
if h.category ~= 'admin' then
table.insert(items, h)
end
end
local page_items, total_pages = get_page(items, page)
if page < 1 then page = total_pages end
if page > total_pages then page = 1 end
page_items, total_pages = get_page(items, page)
local output = format_help_list(page_items)
local keyboard = api.inline_keyboard():row(
api.row():callback_data_button('<', 'help:cmds:' .. (page - 1))
:callback_data_button(page .. '/' .. total_pages, 'help:noop')
:callback_data_button('>', 'help:cmds:' .. (page + 1))
):row(
api.row():callback_data_button('Back', 'help:back')
)
- return api.edit_message_text(message.chat.id, message.message_id, output, 'html', true, keyboard)
+ return api.edit_message_text(message.chat.id, message.message_id, output, {
+ parse_mode = 'html',
+ disable_web_page_preview = true,
+ reply_markup = keyboard
+ })
elseif data:match('^acmds:%d+$') then
local page = tonumber(data:match('^acmds:(%d+)$'))
local items = loader.get_help('admin')
local page_items, total_pages = get_page(items, page)
if page < 1 then page = total_pages end
if page > total_pages then page = 1 end
page_items, total_pages = get_page(items, page)
local output = format_help_list(page_items)
local keyboard = api.inline_keyboard():row(
api.row():callback_data_button('<', 'help:acmds:' .. (page - 1))
:callback_data_button(page .. '/' .. total_pages, 'help:noop')
:callback_data_button('>', 'help:acmds:' .. (page + 1))
):row(
api.row():callback_data_button('Back', 'help:back')
)
- return api.edit_message_text(message.chat.id, message.message_id, output, 'html', true, keyboard)
+ return api.edit_message_text(message.chat.id, message.message_id, output, {
+ parse_mode = 'html',
+ disable_web_page_preview = true,
+ reply_markup = keyboard
+ })
elseif data == 'links' then
local keyboard = api.inline_keyboard():row(
api.row():url_button('Development', 'https://t.me/mattataDev')
:url_button('Channel', 'https://t.me/mattata')
):row(
api.row():url_button('GitHub', 'https://github.com/wrxck/mattata')
:url_button('Support', 'https://t.me/mattataSupport')
):row(
api.row():callback_data_button('Back', 'help:back')
)
- return api.edit_message_text(message.chat.id, message.message_id, 'Useful links:', nil, true, keyboard)
+ return api.edit_message_text(message.chat.id, message.message_id, 'Useful links:', {
+ disable_web_page_preview = true,
+ reply_markup = keyboard
+ })
elseif data == 'settings' then
local permissions = require('src.core.permissions')
if message.chat.type == 'supergroup' and not permissions.is_group_admin(api, message.chat.id, callback_query.from.id) then
return api.answer_callback_query(callback_query.id, 'You need to be an admin to change settings.')
end
local keyboard = api.inline_keyboard():row(
api.row():callback_data_button('Administration', 'administration:' .. message.chat.id .. ':page:1')
:callback_data_button('Plugins', 'plugins:' .. message.chat.id .. ':page:1')
):row(
api.row():callback_data_button('Back', 'help:back')
)
return api.edit_message_reply_markup(message.chat.id, message.message_id, nil, keyboard)
elseif data == 'back' then
local name = tools.escape_html(callback_query.from.first_name)
local output = string.format(
'Hey %s! I\'m <b>%s</b>, a feature-rich Telegram bot.\n\nUse the buttons below to navigate my commands, or type <code>/help &lt;command&gt;</code> for details on a specific command.',
name, tools.escape_html(api.info.first_name)
)
local keyboard = api.inline_keyboard():row(
api.row():callback_data_button('Commands', 'help:cmds:1')
:callback_data_button('Admin Help', 'help:acmds:1')
):row(
api.row():callback_data_button('Links', 'help:links')
:callback_data_button('Settings', 'help:settings')
)
- return api.edit_message_text(message.chat.id, message.message_id, output, 'html', true, keyboard)
+ return api.edit_message_text(message.chat.id, message.message_id, output, {
+ parse_mode = 'html',
+ disable_web_page_preview = true,
+ reply_markup = keyboard
+ })
elseif data == 'noop' then
return api.answer_callback_query(callback_query.id)
end
end
return plugin
diff --git a/src/plugins/utility/id.lua b/src/plugins/utility/id.lua
index fcd1988..e1f05ac 100644
--- a/src/plugins/utility/id.lua
+++ b/src/plugins/utility/id.lua
@@ -1,62 +1,62 @@
--[[
mattata v2.0 - ID Plugin
Returns user/chat ID and information.
]]
local plugin = {}
plugin.name = 'id'
plugin.category = 'utility'
plugin.description = 'Get user or chat ID and info'
plugin.commands = { 'id', 'user', 'whoami' }
plugin.help = '/id [user] - Returns ID and info for the given user, or yourself if no argument is given.'
function plugin.on_message(api, message, ctx)
local tools = require('telegram-bot-lua.tools')
local target = message.from
local input = message.args
-- If replying to someone, use their info
if message.reply and message.reply.from then
target = message.reply.from
elseif input and input ~= '' then
-- Try to resolve username or ID
local resolved = input:match('^@?(.+)$')
local user_id = tonumber(resolved) or ctx.redis.get('username:' .. resolved:lower())
if user_id then
local result = api.get_chat(user_id)
if result and result.result then
target = result.result
end
end
end
local lines = {}
table.insert(lines, '<b>User Information</b>')
table.insert(lines, 'ID: <code>' .. target.id .. '</code>')
table.insert(lines, 'Name: ' .. tools.escape_html(target.first_name or ''))
if target.last_name then
table.insert(lines, 'Last name: ' .. tools.escape_html(target.last_name))
end
if target.username then
table.insert(lines, 'Username: @' .. target.username)
end
if target.language_code then
table.insert(lines, 'Language: <code>' .. target.language_code .. '</code>')
end
-- If in a group, also show chat info
if ctx.is_group then
table.insert(lines, '')
table.insert(lines, '<b>Chat Information</b>')
table.insert(lines, 'ID: <code>' .. message.chat.id .. '</code>')
table.insert(lines, 'Title: ' .. tools.escape_html(message.chat.title or ''))
table.insert(lines, 'Type: ' .. (message.chat.type or 'unknown'))
if message.chat.username then
table.insert(lines, 'Username: @' .. message.chat.username)
end
end
- return api.send_message(message.chat.id, table.concat(lines, '\n'), 'html')
+ return api.send_message(message.chat.id, table.concat(lines, '\n'), { parse_mode = 'html' })
end
return plugin

File Metadata

Mime Type
text/x-diff
Expires
Thu, May 14, 9:30 PM (1 d, 7 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
63576
Default Alt Text
(48 KB)

Event Timeline