+ local question, answer = rest:match('^(.-)%s*|%s*(.+)$')
+ if not question or question == '' or not answer or answer == '' then
+ return api.send_message(chat_id, 'Please separate the question and answer with a pipe character (|).\nExample: <code>/customcaptcha set What colour is the sky? | blue</code>', { parse_mode = 'html' })
+ end
+ question = question:match('^%s*(.-)%s*$')
+ answer = answer:match('^%s*(.-)%s*$')
+ if #question > 300 then
+ return api.send_message(chat_id, 'The question must be 300 characters or fewer.')
+ end
+ if #answer > 100 then
+ return api.send_message(chat_id, 'The answer must be 100 characters or fewer.')
+ 'Welcome, <a href="tg://user?id=%d">%s</a>! To verify you\'re human, please answer the following question:\n\n<b>%s</b>\n\nType your answer in the chat. You have %d seconds.',
+ new_member.id,
+ tools.escape_html(new_member.first_name),
+ tools.escape_html(question),
+ timeout
+ )
+
+ local sent = api.send_message(chat_id, text, { parse_mode = 'html' })
+ { text = 'No, cancel', callback_data = 'delfed:cancel' }
} }
}
return api.send_message(
message.chat.id,
string.format(
'Are you sure you want to delete the federation <b>%s</b>?\n\nThis will remove all bans, chats, and admins associated with it. This action cannot be undone.',
local user_id = ctx.redis.get('username:' .. username)
if user_id then
return tonumber(user_id), '@' .. username
end
end
return nil, nil
end
local function get_chat_federation(db, chat_id)
local result = db.call('sp_get_chat_federation', { chat_id })
if result and #result > 0 then return result[1] end
return nil
end
local function is_fed_admin(db, fed_id, user_id)
local result = db.call('sp_check_federation_admin', { fed_id, user_id })
return result and #result > 0
end
function plugin.on_message(api, message, ctx)
local fed = get_chat_federation(ctx.db, message.chat.id)
if not fed then
return api.send_message(
message.chat.id,
'This chat is not part of any federation.',
- 'html'
+ { parse_mode = 'html' }
)
end
local from_id = message.from.id
if fed.owner_id ~= from_id and not is_fed_admin(ctx.db, fed.id, from_id) then
return api.send_message(
message.chat.id,
'Only the federation owner or a federation admin can manage the allowlist.',
- 'html'
+ { parse_mode = 'html' }
)
end
local target_id, target_name = resolve_user(message, ctx)
if not target_id then
return api.send_message(
message.chat.id,
'Please specify a user to toggle on the allowlist by replying to their message or providing a user ID/username.\nUsage: <code>/fallowlist [user]</code>',
- 'html'
+ { parse_mode = 'html' }
)
end
local existing = ctx.db.call('sp_check_federation_allowlist', { fed.id, target_id })
Creates a new federation. Any user can create up to 5 federations.
]]
local tools = require('telegram-bot-lua.tools')
local plugin = {}
plugin.name = 'newfed'
plugin.category = 'admin'
plugin.description = 'Create a new federation.'
plugin.commands = { 'newfed' }
plugin.help = '/newfed <name> - Create a new federation with the given name.'
plugin.group_only = false
plugin.admin_only = false
function plugin.on_message(api, message, ctx)
local name = message.args
if not name or name == '' then
return api.send_message(
message.chat.id,
'Please specify a name for the federation.\nUsage: <code>/newfed <name></code>',
- 'html'
+ { parse_mode = 'html' }
)
end
if #name > 128 then
return api.send_message(
message.chat.id,
'Federation name must be 128 characters or fewer.',
- 'html'
+ { parse_mode = 'html' }
)
end
local user_id = message.from.id
local existing = ctx.db.call('sp_count_user_federations', { user_id })
if existing and existing[1] and tonumber(existing[1].count) >= 5 then
return api.send_message(
message.chat.id,
'You already own 5 federations, which is the maximum allowed.',
- 'html'
+ { parse_mode = 'html' }
)
end
local result = ctx.db.call('sp_create_federation', { name, user_id })
if not result or #result == 0 then
return api.send_message(
message.chat.id,
'Failed to create the federation. Please try again later.',
- 'html'
+ { parse_mode = 'html' }
)
end
local fed_id = result[1].id
local output = string.format(
'Federation <b>%s</b> created successfully!\n\nFederation ID: <code>%s</code>\n\nUse <code>/joinfed %s</code> in a group to add it to this federation.',
plugin.description = 'Import settings from another chat'
plugin.commands = { 'import' }
plugin.help = '/import <chat_id> - Imports settings, filters, triggers, and rules from another chat.'
plugin.group_only = true
plugin.admin_only = true
plugin.global_admin_only = true
function plugin.on_message(api, message, ctx)
if not message.args then
return api.send_message(message.chat.id, 'Usage: /import <chat_id>\n\nImports settings, filters, triggers, rules, and welcome messages from another chat.')
end
local source_id = tonumber(message.args)
if not source_id then
return api.send_message(message.chat.id, 'Please provide a valid chat ID.')
end
if source_id == message.chat.id then
return api.send_message(message.chat.id, 'You can\'t import from the same chat.')
end
+ -- Verify the calling user is a member of the source chat
+ local member = api.get_chat_member(source_id, message.from.id)
+ if not member or not member.result or member.result.status == 'left' or member.result.status == 'kicked' then
+ return api.send_message(message.chat.id, 'You must be a member of the source chat to import from it.')
+ end
+
local imported = {}
-- Import chat_settings
- local settings = ctx.db.execute(
- 'SELECT key, value FROM chat_settings WHERE chat_id = $1',
- { source_id }
- )
+ local settings = ctx.db.call('sp_get_all_chat_settings', { source_id })
if settings and #settings > 0 then
for _, s in ipairs(settings) do
ctx.db.upsert('chat_settings', {
chat_id = message.chat.id,
key = s.key,
value = s.value
}, { 'chat_id', 'key' }, { 'value' })
end
table.insert(imported, #settings .. ' settings')
end
-- Import filters
- local filters = ctx.db.execute(
- 'SELECT pattern, action, response FROM filters WHERE chat_id = $1',
- { source_id }
- )
+ local filters = ctx.db.call('sp_get_filters_full', { source_id })
if filters and #filters > 0 then
for _, f in ipairs(filters) do
- local existing = ctx.db.execute(
- 'SELECT 1 FROM filters WHERE chat_id = $1 AND pattern = $2',
- { message.chat.id, f.pattern }
- )
+ local existing = ctx.db.call('sp_get_filter', { message.chat.id, f.pattern })
if not existing or #existing == 0 then
ctx.db.insert('filters', {
chat_id = message.chat.id,
pattern = f.pattern,
action = f.action,
response = f.response,
created_by = message.from.id
})
end
end
table.insert(imported, #filters .. ' filters')
end
-- Import triggers
- local triggers = ctx.db.execute(
- 'SELECT pattern, response, is_media, file_id FROM triggers WHERE chat_id = $1',
- { source_id }
- )
+ local triggers = ctx.db.call('sp_get_triggers', { source_id })
if triggers and #triggers > 0 then
for _, t in ipairs(triggers) do
- local existing = ctx.db.execute(
- 'SELECT 1 FROM triggers WHERE chat_id = $1 AND pattern = $2',
- { message.chat.id, t.pattern }
- )
+ local existing = ctx.db.call('sp_check_trigger_exists', { message.chat.id, t.pattern })
if not existing or #existing == 0 then
ctx.db.insert('triggers', {
chat_id = message.chat.id,
pattern = t.pattern,
response = t.response,
is_media = t.is_media,
file_id = t.file_id,
created_by = message.from.id
})
end
end
table.insert(imported, #triggers .. ' triggers')
end
-- Import rules
- local rules = ctx.db.execute(
- 'SELECT rules_text FROM rules WHERE chat_id = $1',
- { source_id }
- )
+ local rules = ctx.db.call('sp_get_rules', { source_id })
if rules and #rules > 0 then
ctx.db.upsert('rules', {
chat_id = message.chat.id,
rules_text = rules[1].rules_text
}, { 'chat_id' }, { 'rules_text' })
table.insert(imported, 'rules')
end
-- Import welcome message
- local welcome = ctx.db.execute(
- 'SELECT message, parse_mode FROM welcome_messages WHERE chat_id = $1',
- { source_id }
- )
+ local welcome = ctx.db.call('sp_get_welcome_message_full', { source_id })
if welcome and #welcome > 0 then
ctx.db.upsert('welcome_messages', {
chat_id = message.chat.id,
message = welcome[1].message,
parse_mode = welcome[1].parse_mode
}, { 'chat_id' }, { 'message', 'parse_mode' })
table.insert(imported, 'welcome message')
end
-- Import allowed links
- local links = ctx.db.execute(
- 'SELECT link FROM allowed_links WHERE chat_id = $1',
- { source_id }
- )
+ local links = ctx.db.call('sp_get_allowed_links', { source_id })
if links and #links > 0 then
for _, l in ipairs(links) do
- local existing = ctx.db.execute(
- 'SELECT 1 FROM allowed_links WHERE chat_id = $1 AND link = $2',
- { message.chat.id, l.link }
- )
+ local existing = ctx.db.call('sp_check_allowed_link', { message.chat.id, l.link })
Batch-deletes messages using delete_messages API for efficiency.
]]
local plugin = {}
plugin.name = 'purge'
plugin.category = 'admin'
plugin.description = 'Delete messages in bulk'
plugin.commands = { 'purge' }
plugin.help = '/purge - Deletes all messages from the replied-to message up to the command message.'
plugin.group_only = true
plugin.admin_only = true
local BATCH_SIZE = 100
function plugin.on_message(api, message, ctx)
local permissions = require('src.core.permissions')
if not permissions.can_delete(api, message.chat.id) then
return api.send_message(message.chat.id, 'I need the "Delete Messages" admin permission to use this command.')
end
if not message.reply then
return api.send_message(message.chat.id, 'Please reply to the first message you want to delete, and all messages from that point to your command will be purged.')
end
local start_id = message.reply.message_id
local end_id = message.message_id
local count = 0
local failed = 0
-- Batch into groups of up to 100 and use delete_messages
local batch = {}
for msg_id = start_id, end_id do
table.insert(batch, msg_id)
if #batch >= BATCH_SIZE then
local success = api.delete_messages(message.chat.id, batch)
if success then
count = count + #batch
else
failed = failed + #batch
end
batch = {}
end
end
-- Delete remaining messages
if #batch > 0 then
local success = api.delete_messages(message.chat.id, batch)
+plugin.help = '/reactions <on|off> - Toggle reaction karma tracking for this group.'
+plugin.group_only = true
+plugin.admin_only = true
+
+local THUMBS_UP = '\xF0\x9F\x91\x8D'
+local THUMBS_DOWN = '\xF0\x9F\x91\x8E'
+
+function plugin.on_message(api, message, ctx)
+ if not message.args or message.args == '' then
+ -- Show current status
+ local enabled = session.get_cached_setting(message.chat.id, 'reactions_enabled', function()
+ local result = ctx.db.call('sp_get_chat_setting', { message.chat.id, 'reactions_enabled' })
+ if result and #result > 0 then
+ return result[1].value
+ end
+ return nil
+ end)
+ local status = (enabled == 'true') and 'enabled' or 'disabled'
+ return api.send_message(message.chat.id,
+ string.format('Reaction karma is currently <b>%s</b> for this group.\nUse <code>/reactions on</code> or <code>/reactions off</code> to toggle.', status),
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.'
function plugin.on_message(api, message, ctx)
- local https = require('ssl.https')
- local json = require('dkjson')
+ local http = require('src.core.http')
local url = require('socket.url')
- local ltn12 = require('ltn12')
local tenor_key = ctx.config.get('TENOR_API_KEY')
if not tenor_key or tenor_key == '' then
return api.send_message(message.chat.id, 'The Tenor API key is not configured. Please set <code>TENOR_API_KEY</code> in the bot configuration.', 'html')
end
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' })
- return api.send_message(message.chat.id, 'Please reply to a sticker or image with an emoji, e.g. <code>/addsticker [emoji]</code>.', 'html')
+ return api.send_message(message.chat.id, 'Please reply to a sticker or image with an emoji, e.g. <code>/addsticker [emoji]</code>.', { parse_mode = 'html' })
end
local emoji = message.args and message.args:match('^(%S+)') or nil
if not emoji then
-- Default emoji if none provided
emoji = message.reply.sticker and message.reply.sticker.emoji or '\xF0\x9F\x98\x80'
end
local bot_username = api.info.username
local set_name = get_sticker_set_name(bot_username)
local user_id = message.from.id
local sticker_input
if message.reply.sticker then
-- Use the sticker file directly
sticker_input = message.reply.sticker.file_id
elseif message.reply.photo then
-- Use the largest photo size
local photos = message.reply.photo
sticker_input = photos[#photos].file_id
elseif message.reply.document and message.reply.document.mime_type and message.reply.document.mime_type:match('^image/') then
sticker_input = message.reply.document.file_id
else
return api.send_message(message.chat.id, 'Please reply to a sticker or image.')
end
-- Build the sticker input for the API
local sticker_data = {
sticker = sticker_input,
emoji_list = { emoji },
format = 'static'
}
-- Check if the sticker from the reply is animated/video and set format accordingly
if message.reply.sticker then
if message.reply.sticker.is_animated then
sticker_data.format = 'animated'
elseif message.reply.sticker.is_video then
sticker_data.format = 'video'
end
end
-- Try to add to existing set first
local success = api.add_sticker_to_set(user_id, set_name, sticker_data)
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 owner_id = config.get_list('BOT_ADMINS')[1] or '221714512'
+ local github_url = config.get('GITHUB_URL', 'https://github.com/wrxck/mattata')
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
+ 'Created by <a href="tg://user?id=%s">Matt</a>. Powered by <code>mattata v%s</code>. Source code available <a href="%s">on GitHub</a>.',
-- 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 <command></code> for details on a specific command.',
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 <command></code> for details on a specific command.',
plugin.description = 'View your Last.fm now playing and recent tracks'
plugin.commands = { 'lastfm', 'np', 'fmset' }
plugin.help = '/np - Show your currently playing or most recent track.\n/fmset <username> - Link your Last.fm account.\n/lastfm [username] - View recent tracks for a Last.fm user.'
function plugin.on_message(api, message, ctx)
- local https = require('ssl.https')
- local json = require('dkjson')
+ local http = require('src.core.http')
local url = require('socket.url')
local tools = require('telegram-bot-lua.tools')
local config = require('src.core.config')
local api_key = config.get('LASTFM_API_KEY')
if not api_key or api_key == '' then
return api.send_message(message.chat.id, 'Last.fm is not configured. The bot admin needs to set LASTFM_API_KEY.')
end
-- /fmset: link Last.fm username
if message.command == 'fmset' then
local username = message.args
if not username or username == '' then
return api.send_message(message.chat.id, 'Please provide your Last.fm username. Usage: /fmset <username>')
.. '<code>/remind 2h30m meeting with John</code>\n'
.. '<code>/remind 1d renew subscription</code>',
- 'html'
+ { parse_mode = 'html' }
)
end
-- Parse duration from the first token
local duration_str, reminder_text = input:match('^(%S+)%s+(.+)$')
if not duration_str then
-- Maybe just a duration with no text
duration_str = input
reminder_text = nil
end
local duration = parse_duration(duration_str)
if not duration then
return api.send_message(
message.chat.id,
'Invalid duration format. Use combinations like: <code>30m</code>, <code>2h</code>, <code>1d</code>, <code>2h30m</code>',
- 'html'
+ { parse_mode = 'html' }
)
end
if not reminder_text or reminder_text == '' then
return api.send_message(message.chat.id, 'Please include a reminder message after the duration.')
end
if duration < 30 then
return api.send_message(message.chat.id, 'Minimum reminder duration is 30 seconds.')
end
if duration > MAX_DURATION then
return api.send_message(message.chat.id, 'Maximum reminder duration is 7 days.')
end
-- Check reminder limit
local existing = get_user_reminders(redis, message.chat.id, message.from.id)
-- Filter to only count non-expired ones
local active_count = 0
for _, key in ipairs(existing) do
local expires = redis.hget(key, 'expires')
if expires and tonumber(expires) > os.time() then
active_count = active_count + 1
else
-- Clean up expired entry
redis.del(key)
end
end
if active_count >= MAX_REMINDERS then
return api.send_message(
message.chat.id,
string.format('You already have %d active reminders in this chat (max %d). Wait for one to expire or use /reminders to check them.', active_count, MAX_REMINDERS)
+local function handle_cancel(api, message, ctx, id_str)
+ local redis = ctx.redis
+ local id = tonumber(id_str)
+
+ if not id then
+ return api.send_message(message.chat.id, 'Please provide a valid message ID to cancel.\nUsage: <code>/schedule cancel 3</code>', { parse_mode = 'html' })
+ end
+
+ local key = hash_key(message.chat.id, id)
+ local exists = redis.hget(key, 'chat_id')
+
+ if not exists then
+ return api.send_message(message.chat.id, string.format('Scheduled message #%d not found.', id))
+ end
+
+ redis.del(key)
+ redis.srem(index_key(message.chat.id), key)
+
+ return api.send_message(message.chat.id, string.format('Scheduled message #%d has been cancelled.', id))
+end
+
+-- Handle /schedule <duration> <message>
+local function handle_schedule(api, message, ctx)
+ local redis = ctx.redis
+ local input = message.args
+
+ if not input or input == '' then
+ return api.send_message(
+ message.chat.id,
+ 'Usage:\n'
+ .. '<code>/schedule <duration> <message></code> - Schedule a message\n'
+ .. '<code>/schedule list</code> - List pending messages\n'
+ .. '<code>/schedule cancel <id></code> - Cancel a message\n\n'
plugin.description = 'Translate text between languages'
plugin.commands = { 'translate', 'tl' }
plugin.help = '/translate [lang] <text> - Translate text to the specified language (default: en). Reply to a message to translate it, or provide text directly.'
-local https = require('ssl.https')
+local http = require('src.core.http')
local json = require('dkjson')
-local ltn12 = require('ltn12')
local tools = require('telegram-bot-lua.tools')
local BASE_URL = 'https://libretranslate.com'
-- Common language code aliases
local LANG_ALIASES = {
english = 'en', en = 'en',
spanish = 'es', es = 'es',
french = 'fr', fr = 'fr',
german = 'de', de = 'de',
italian = 'it', it = 'it',
portuguese = 'pt', pt = 'pt',
russian = 'ru', ru = 'ru',
chinese = 'zh', zh = 'zh',
japanese = 'ja', ja = 'ja',
korean = 'ko', ko = 'ko',
arabic = 'ar', ar = 'ar',
hindi = 'hi', hi = 'hi',
dutch = 'nl', nl = 'nl',
polish = 'pl', pl = 'pl',
turkish = 'tr', tr = 'tr',
swedish = 'sv', sv = 'sv',
czech = 'cs', cs = 'cs',
romanian = 'ro', ro = 'ro',
hungarian = 'hu', hu = 'hu',
ukrainian = 'uk', uk = 'uk',
indonesian = 'id', id = 'id',
finnish = 'fi', fi = 'fi',
hebrew = 'he', he = 'he',
thai = 'th', th = 'th',
vietnamese = 'vi', vi = 'vi',
greek = 'el', el = 'el'
}
local function translate_text(text, target, source)
source = source or 'auto'
local request_body = json.encode({
q = text,
source = source,
target = target,
format = 'text'
})
- local body = {}
- local _, code = https.request({
- url = BASE_URL .. '/translate',
- method = 'POST',
- headers = {
- ['Content-Type'] = 'application/json',
- ['Content-Length'] = tostring(#request_body)
- },
- source = ltn12.source.string(request_body),
- sink = ltn12.sink.table(body)
- })
+ local body, code = http.post(BASE_URL .. '/translate', request_body, 'application/json')
if code ~= 200 then
return nil, 'Translation service returned an error (HTTP ' .. tostring(code) .. '). The public instance may be rate-limited; try again shortly.'
end
- local data = json.decode(table.concat(body))
+ local data = json.decode(body)
if not data then
return nil, 'Failed to parse translation response.'