diff --git a/.busted b/.busted index 466287d..6fc1494 100644 --- a/.busted +++ b/.busted @@ -5,5 +5,11 @@ return { helper = "spec/test_helper.lua", lpath = "./src/?.lua;./src/?/init.lua", verbose = true + }, + e2e = { + ROOT = {"spec"}, + pattern = "e2e_test", + lpath = "./src/?.lua;./src/?/init.lua", + verbose = true } } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78b3df5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +*.env diff --git a/spec/builders_spec.lua b/spec/builders_spec.lua index dca3728..705bbc0 100644 --- a/spec/builders_spec.lua +++ b/spec/builders_spec.lua @@ -213,9 +213,9 @@ describe('builders', function() assert.equals('HTML', c.parse_mode) end) - it('converts boolean true to markdown', function() + it('converts boolean true to MarkdownV2', function() local c = api.input_text_message_content('Hello', true) - assert.equals('markdown', c.parse_mode) + assert.equals('MarkdownV2', c.parse_mode) end) end) diff --git a/spec/e2e_test.lua b/spec/e2e_test.lua new file mode 100644 index 0000000..aaec111 --- /dev/null +++ b/spec/e2e_test.lua @@ -0,0 +1,948 @@ +-- End-to-end integration tests using the real Telegram Bot API +-- Requires a .env file with BOT_TOKEN set +-- Run with: busted --run e2e + +-- Load .env file +local function load_env() + local f = io.open('.env', 'r') + if not f then return nil end + local env = {} + for line in f:lines() do + local key, value = line:match('^([%w_]+)%s*=%s*(.+)$') + if key and value then + env[key] = value + end + end + f:close() + return env +end + +local env = load_env() +if not env or not env.BOT_TOKEN then + print('Skipping e2e tests: no .env file or BOT_TOKEN not set') + return +end + +_G._TEST = true +package.path = './src/?.lua;./src/?/init.lua;' .. package.path + +local module_map = { + ['telegram-bot-lua'] = 'src/init.lua', + ['telegram-bot-lua.config'] = 'src/config.lua', + ['telegram-bot-lua.handlers'] = 'src/handlers.lua', + ['telegram-bot-lua.builders'] = 'src/builders.lua', + ['telegram-bot-lua.helpers'] = 'src/helpers.lua', + ['telegram-bot-lua.tools'] = 'src/tools.lua', + ['telegram-bot-lua.utils'] = 'src/utils.lua', + ['telegram-bot-lua.async'] = 'src/async.lua', + ['telegram-bot-lua.compat'] = 'src/compat.lua', + ['telegram-bot-lua.core'] = 'src/core.lua', + ['telegram-bot-lua.polyfill'] = 'src/polyfill.lua', + ['telegram-bot-lua.b64url'] = 'src/b64url.lua', + ['telegram-bot-lua.methods.updates'] = 'src/methods/updates.lua', + ['telegram-bot-lua.methods.messages'] = 'src/methods/messages.lua', + ['telegram-bot-lua.methods.chat'] = 'src/methods/chat.lua', + ['telegram-bot-lua.methods.members'] = 'src/methods/members.lua', + ['telegram-bot-lua.methods.forum'] = 'src/methods/forum.lua', + ['telegram-bot-lua.methods.stickers'] = 'src/methods/stickers.lua', + ['telegram-bot-lua.methods.inline'] = 'src/methods/inline.lua', + ['telegram-bot-lua.methods.payments'] = 'src/methods/payments.lua', + ['telegram-bot-lua.methods.games'] = 'src/methods/games.lua', + ['telegram-bot-lua.methods.passport'] = 'src/methods/passport.lua', + ['telegram-bot-lua.methods.bot'] = 'src/methods/bot.lua', + ['telegram-bot-lua.methods.gifts'] = 'src/methods/gifts.lua', + ['telegram-bot-lua.methods.checklists'] = 'src/methods/checklists.lua', + ['telegram-bot-lua.methods.stories'] = 'src/methods/stories.lua', + ['telegram-bot-lua.methods.suggested_posts'] = 'src/methods/suggested_posts.lua', + ['telegram-bot-lua.adapters'] = 'src/adapters/init.lua', + ['telegram-bot-lua.adapters.db'] = 'src/adapters/db.lua', + ['telegram-bot-lua.adapters.redis'] = 'src/adapters/redis.lua', + ['telegram-bot-lua.adapters.llm'] = 'src/adapters/llm.lua', + ['telegram-bot-lua.adapters.email'] = 'src/adapters/email.lua', +} +for mod_name, file_path in pairs(module_map) do + if not package.preload[mod_name] then + package.preload[mod_name] = function() + return dofile(file_path) + end + end +end + +local api = require('telegram-bot-lua') +api.token = env.BOT_TOKEN + +-- Get bot info for use in tests +local bot_info = api.get_me() +local bot_id = bot_info and bot_info.result and bot_info.result.id + +describe('e2e', function() + + -- ========================================================================= + -- Bot identity + -- ========================================================================= + describe('bot identity', function() + it('get_me returns bot info', function() + local result = api.get_me() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.is_true(result.result.is_bot) + assert.is_string(result.result.first_name) + assert.is_number(result.result.id) + assert.is_string(result.result.username) + end) + end) + + -- ========================================================================= + -- Bot commands (default scope) + -- ========================================================================= + describe('bot commands (default scope)', function() + it('set_my_commands succeeds', function() + local commands = { + api.bot_command('start', 'Start the bot'), + api.bot_command('help', 'Show help') + } + local result = api.set_my_commands(commands) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_commands returns the commands', function() + local result = api.get_my_commands() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.equals(2, #result.result) + assert.equals('start', result.result[1].command) + assert.equals('help', result.result[2].command) + end) + + it('delete_my_commands succeeds', function() + local result = api.delete_my_commands() + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_commands returns empty after delete', function() + local result = api.get_my_commands() + assert.is_table(result) + assert.is_true(result.ok) + assert.equals(0, #result.result) + end) + end) + + -- ========================================================================= + -- Bot commands (with scope and language_code) + -- ========================================================================= + describe('bot commands (scoped)', function() + it('set_my_commands with all_private_chats scope', function() + local commands = { + api.bot_command('settings', 'Bot settings'), + } + local result = api.set_my_commands(commands, { + scope = { type = 'all_private_chats' } + }) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_commands with all_private_chats scope', function() + local result = api.get_my_commands({ + scope = { type = 'all_private_chats' } + }) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.equals(1, #result.result) + assert.equals('settings', result.result[1].command) + end) + + it('set_my_commands with language_code', function() + local commands = { + api.bot_command('inicio', 'Iniciar el bot'), + } + local result = api.set_my_commands(commands, { + language_code = 'es' + }) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_commands with language_code', function() + local result = api.get_my_commands({ language_code = 'es' }) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.equals(1, #result.result) + assert.equals('inicio', result.result[1].command) + end) + + it('cleanup: delete scoped commands', function() + local r1 = api.delete_my_commands({ scope = { type = 'all_private_chats' } }) + assert.is_true(r1.ok) + local r2 = api.delete_my_commands({ language_code = 'es' }) + assert.is_true(r2.ok) + end) + end) + + -- ========================================================================= + -- Bot name + -- ========================================================================= + describe('bot name', function() + local original_name + + it('get_my_name returns name', function() + local result = api.get_my_name() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + original_name = result.result.name or '' + end) + + it('set_my_name succeeds', function() + local result = api.set_my_name('E2E Test Bot Name') + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_name confirms change', function() + local result = api.get_my_name() + assert.is_table(result) + assert.is_true(result.ok) + assert.equals('E2E Test Bot Name', result.result.name) + end) + + it('set_my_name with language_code', function() + local result = api.set_my_name('Bot de prueba', { language_code = 'es' }) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_name with language_code', function() + local result = api.get_my_name({ language_code = 'es' }) + assert.is_table(result) + assert.is_true(result.ok) + assert.equals('Bot de prueba', result.result.name) + end) + + it('cleanup: reset name for es', function() + local result = api.set_my_name('', { language_code = 'es' }) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('restores original name', function() + local result = api.set_my_name(original_name) + assert.is_table(result) + assert.is_true(result.ok) + end) + end) + + -- ========================================================================= + -- Bot description + -- ========================================================================= + describe('bot description', function() + local original_description + + it('get_my_description returns description', function() + local result = api.get_my_description() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + original_description = result.result.description or '' + end) + + it('set_my_description succeeds', function() + local result = api.set_my_description('E2E test bot description') + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_description confirms change', function() + local result = api.get_my_description() + assert.is_table(result) + assert.is_true(result.ok) + assert.equals('E2E test bot description', result.result.description) + end) + + it('restores original description', function() + local result = api.set_my_description(original_description) + assert.is_table(result) + assert.is_true(result.ok) + end) + end) + + -- ========================================================================= + -- Bot short description + -- ========================================================================= + describe('bot short description', function() + local original_short_description + + it('get_my_short_description returns short description', function() + local result = api.get_my_short_description() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + original_short_description = result.result.short_description or '' + end) + + it('set_my_short_description succeeds', function() + local result = api.set_my_short_description('E2E short desc') + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_short_description confirms change', function() + local result = api.get_my_short_description() + assert.is_table(result) + assert.is_true(result.ok) + assert.equals('E2E short desc', result.result.short_description) + end) + + it('restores original short description', function() + local result = api.set_my_short_description(original_short_description) + assert.is_table(result) + assert.is_true(result.ok) + end) + end) + + -- ========================================================================= + -- Default administrator rights + -- ========================================================================= + describe('default administrator rights', function() + local original_rights + + it('get_my_default_administrator_rights returns rights', function() + local result = api.get_my_default_administrator_rights() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + original_rights = result.result + end) + + it('set_my_default_administrator_rights succeeds', function() + local result = api.set_my_default_administrator_rights({ + rights = { + can_manage_chat = true, + can_delete_messages = true, + can_manage_video_chats = false, + can_restrict_members = false, + can_promote_members = false, + can_change_info = true, + can_invite_users = true, + can_post_stories = false, + can_edit_stories = false, + can_delete_stories = false, + can_pin_messages = true, + can_manage_topics = false, + is_anonymous = false + } + }) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_my_default_administrator_rights confirms change', function() + local result = api.get_my_default_administrator_rights() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_true(result.result.can_manage_chat) + assert.is_true(result.result.can_delete_messages) + assert.is_true(result.result.can_change_info) + assert.is_true(result.result.can_invite_users) + assert.is_true(result.result.can_pin_messages) + end) + + it('get_my_default_administrator_rights for channels', function() + local result = api.get_my_default_administrator_rights({ for_channels = true }) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + end) + + it('restores original rights', function() + local result = api.set_my_default_administrator_rights({ + rights = original_rights + }) + assert.is_table(result) + assert.is_true(result.ok) + end) + end) + + -- ========================================================================= + -- Chat menu button + -- ========================================================================= + describe('chat menu button', function() + it('get_chat_menu_button returns default', function() + local result = api.get_chat_menu_button() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.is_string(result.result.type) + end) + + it('set_chat_menu_button to commands type', function() + local result = api.set_chat_menu_button({ + menu_button = { type = 'commands' } + }) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_chat_menu_button confirms commands type', function() + local result = api.get_chat_menu_button() + assert.is_table(result) + assert.is_true(result.ok) + assert.equals('commands', result.result.type) + end) + + it('set_chat_menu_button to default type', function() + local result = api.set_chat_menu_button({ + menu_button = { type = 'default' } + }) + assert.is_table(result) + assert.is_true(result.ok) + end) + end) + + -- ========================================================================= + -- Webhook management + -- ========================================================================= + describe('webhook management', function() + it('delete_webhook succeeds', function() + local result = api.delete_webhook() + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_webhook_info shows no webhook', function() + local result = api.get_webhook_info() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.equals('', result.result.url) + end) + + it('set_webhook succeeds', function() + local result = api.set_webhook('https://example.com/e2e-test-webhook') + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_webhook_info confirms webhook', function() + local result = api.get_webhook_info() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.equals('https://example.com/e2e-test-webhook', result.result.url) + end) + + it('cleans up webhook', function() + local result = api.delete_webhook({ drop_pending_updates = true }) + assert.is_table(result) + assert.is_true(result.ok) + end) + + it('get_webhook_info confirms cleanup', function() + local result = api.get_webhook_info() + assert.is_table(result) + assert.is_true(result.ok) + assert.equals('', result.result.url) + end) + end) + + -- ========================================================================= + -- Updates + -- ========================================================================= + describe('get_updates', function() + it('returns updates array', function() + local result = api.get_updates({ timeout = 0, limit = 1 }) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + end) + + it('returns updates with allowed_updates filter', function() + local result = api.get_updates({ + timeout = 0, + limit = 1, + allowed_updates = { 'message' } + }) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + end) + end) + + -- ========================================================================= + -- Star transactions + -- ========================================================================= + describe('star transactions', function() + it('get_star_transactions returns transactions', function() + local result = api.get_star_transactions() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + end) + + it('get_star_transactions with opts', function() + local result = api.get_star_transactions({ offset = 0, limit = 10 }) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + end) + end) + + -- ========================================================================= + -- Forum topic icon stickers + -- ========================================================================= + describe('forum topic icon stickers', function() + it('get_forum_topic_icon_stickers returns sticker list', function() + local result = api.get_forum_topic_icon_stickers() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + -- Should return a non-empty list of stickers + assert.is_true(#result.result > 0) + -- Each sticker should have basic fields + local sticker = result.result[1] + assert.is_string(sticker.file_id) + assert.is_string(sticker.file_unique_id) + assert.is_string(sticker.type) + end) + end) + + -- ========================================================================= + -- User profile photos (using bot's own ID) + -- ========================================================================= + describe('user profile photos', function() + it('get_user_profile_photos returns photos', function() + if not bot_id then pending('no bot_id') end + local result = api.get_user_profile_photos(bot_id) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.is_number(result.result.total_count) + assert.is_table(result.result.photos) + end) + + it('get_user_profile_photos with offset and limit', function() + if not bot_id then pending('no bot_id') end + local result = api.get_user_profile_photos(bot_id, { offset = 0, limit = 1 }) + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + assert.is_number(result.result.total_count) + end) + end) + + -- ========================================================================= + -- Available gifts + -- ========================================================================= + describe('available gifts', function() + it('get_available_gifts returns gift list', function() + local result = api.get_available_gifts() + assert.is_table(result) + assert.is_true(result.ok) + assert.is_table(result.result) + end) + end) + + -- ========================================================================= + -- Sticker sets (using known public sets) + -- ========================================================================= + describe('sticker sets', function() + it('get_sticker_set returns set info for known set', function() + local result = api.get_sticker_set('AnimatedEmojies') + if result and result.ok then + assert.is_table(result.result) + assert.equals('AnimatedEmojies', result.result.name) + assert.is_string(result.result.title) + assert.is_table(result.result.stickers) + assert.is_true(#result.result.stickers > 0) + else + -- Set may not exist, that's fine - verify error structure + assert.is_false(result) + end + end) + + it('get_sticker_set returns error for invalid name', function() + local result, err = api.get_sticker_set('totally_nonexistent_sticker_set_12345') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + end) + + -- ========================================================================= + -- Create invoice link (Telegram Stars / XTR currency) + -- ========================================================================= + describe('create invoice link', function() + it('create_invoice_link returns a URL', function() + local prices = {{ label = 'Test Item', amount = 1 }} + local result = api.create_invoice_link( + 'E2E Test Invoice', + 'Test description for e2e', + 'e2e_test_payload_' .. os.time(), + 'XTR', + prices + ) + if result and result.ok then + assert.is_string(result.result) + -- Telegram invoice links start with https:// + assert.truthy(result.result:match('^https://')) + else + -- If bot doesn't have payments enabled, verify error structure + assert.is_false(result) + end + end) + end) + + -- ========================================================================= + -- Error handling + -- ========================================================================= + describe('error handling', function() + it('send_message returns error for invalid chat_id', function() + local result, err = api.send_message(0, 'test') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('get_file returns error for invalid file_id', function() + local result, err = api.get_file('invalid_file_id_that_does_not_exist') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('get_chat returns error for invalid chat_id', function() + local result, err = api.get_chat(0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('get_chat_member returns error for invalid params', function() + local result, err = api.get_chat_member(0, 0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_photo returns error for invalid chat_id', function() + local result, err = api.send_photo(0, 'invalid_photo') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_video returns error for invalid chat_id', function() + local result, err = api.send_video(0, 'invalid_video') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_audio returns error for invalid chat_id', function() + local result, err = api.send_audio(0, 'invalid_audio') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_document returns error for invalid chat_id', function() + local result, err = api.send_document(0, 'invalid_doc') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_animation returns error for invalid chat_id', function() + local result, err = api.send_animation(0, 'invalid_anim') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_voice returns error for invalid chat_id', function() + local result, err = api.send_voice(0, 'invalid_voice') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_video_note returns error for invalid chat_id', function() + local result, err = api.send_video_note(0, 'invalid_videonote') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_sticker returns error for invalid chat_id', function() + local result, err = api.send_sticker(0, 'invalid_sticker') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_location returns error for invalid chat_id', function() + local result, err = api.send_location(0, 51.5074, -0.1278) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_venue returns error for invalid chat_id', function() + local result, err = api.send_venue(0, 51.5074, -0.1278, 'Test Venue', '123 Test St') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_contact returns error for invalid chat_id', function() + local result, err = api.send_contact(0, '+1234567890', 'Test') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_poll returns error for invalid chat_id', function() + local options = { + { text = 'Option A' }, + { text = 'Option B' } + } + local result, err = api.send_poll(0, 'Test question?', options) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_dice returns error for invalid chat_id', function() + local result, err = api.send_dice(0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_chat_action returns error for invalid chat_id', function() + local result, err = api.send_chat_action(0, 'typing') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('forward_message returns error for invalid params', function() + local result, err = api.forward_message(0, 0, 0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('copy_message returns error for invalid params', function() + local result, err = api.copy_message(0, 0, 0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('edit_message_text returns error for invalid params', function() + local result, err = api.edit_message_text(0, 0, 'test') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('delete_message returns error for invalid params', function() + local result, err = api.delete_message(0, 0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('ban_chat_member returns error for invalid params', function() + local result, err = api.ban_chat_member(0, 0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('pin_chat_message returns error for invalid params', function() + local result, err = api.pin_chat_message(0, 0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('create_forum_topic returns error for invalid chat', function() + local result, err = api.create_forum_topic(0, 'Test Topic') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_invoice returns error for invalid chat_id', function() + local prices = {{ label = 'Item', amount = 100 }} + local result, err = api.send_invoice(0, 'Title', 'Desc', 'payload', 'XTR', prices) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('get_user_gifts returns error for invalid user', function() + local result, err = api.get_user_gifts(0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('answer_inline_query returns error for invalid query_id', function() + local result, err = api.answer_inline_query('invalid_id', {}) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('answer_callback_query returns error for invalid query_id', function() + local result, err = api.answer_callback_query('invalid_id') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('leave_chat returns error for invalid chat_id', function() + local result, err = api.leave_chat(0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('export_chat_invite_link returns error for invalid chat', function() + local result, err = api.export_chat_invite_link(0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('get_chat_member_count returns error for invalid chat', function() + local result, err = api.get_chat_member_count(0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('set_chat_title returns error for invalid chat', function() + local result, err = api.set_chat_title(0, 'Test') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('set_chat_description returns error for invalid chat', function() + local result, err = api.set_chat_description(0, 'Test') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('get_chat_administrators returns error for invalid chat', function() + local result, err = api.get_chat_administrators(0) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('set_chat_permissions returns error for invalid chat', function() + local result, err = api.set_chat_permissions(0, { can_send_messages = true }) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('send_game returns error for invalid chat_id', function() + local result, err = api.send_game(0, 'testgame') + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + + it('set_passport_data_errors returns error for invalid user', function() + local result, err = api.set_passport_data_errors(0, {}) + assert.is_false(result) + assert.is_table(err) + assert.is_number(err.error_code) + end) + end) + + -- ========================================================================= + -- Custom emoji stickers + -- ========================================================================= + describe('custom emoji stickers', function() + it('get_custom_emoji_stickers returns error for invalid IDs', function() + local result, err = api.get_custom_emoji_stickers({'invalid_emoji_id'}) + -- Should either succeed with empty array or fail gracefully + if result then + assert.is_table(result) + assert.is_true(result.ok) + else + assert.is_false(result) + assert.is_table(err) + end + end) + end) + + -- ========================================================================= + -- Helpers (no-network, validate logic only) + -- ========================================================================= + describe('helpers', function() + it('send_reply returns false for invalid message', function() + local result = api.send_reply(nil, 'test') + assert.is_false(result) + end) + + it('send_reply returns false for message missing chat', function() + local result = api.send_reply({ message_id = 1 }, 'test') + assert.is_false(result) + end) + + it('send_reply returns false for message missing message_id', function() + local result = api.send_reply({ chat = { id = 1 } }, 'test') + assert.is_false(result) + end) + end) + + -- ========================================================================= + -- Builder utilities (no-network, validate constructors) + -- ========================================================================= + describe('builders', function() + it('bot_command builds correct structure', function() + local cmd = api.bot_command('test', 'Test command') + assert.is_table(cmd) + assert.equals('test', cmd.command) + assert.equals('Test command', cmd.description) + end) + + it('keyboard builder works', function() + local kb = api.keyboard(true, false, false) + assert.is_table(kb) + assert.is_table(kb.keyboard) + assert.is_true(kb.resize_keyboard) + end) + + it('inline_keyboard builder works', function() + local ikb = api.inline_keyboard() + assert.is_table(ikb) + assert.is_table(ikb.inline_keyboard) + end) + + it('row builder supports chaining', function() + local r = api.row() + assert.is_table(r) + local chained = r:callback_data_button('btn', 'data') + assert.equals(r, chained) + assert.equals(1, #r) + assert.equals('btn', r[1].text) + assert.equals('data', r[1].callback_data) + end) + end) +end) diff --git a/spec/methods_spec.lua b/spec/methods_spec.lua index f289f99..818e669 100644 --- a/spec/methods_spec.lua +++ b/spec/methods_spec.lua @@ -31,7 +31,7 @@ describe('methods', function() assert.is_true(req.parameters.disable_notification) end) - it('send_message converts boolean parse_mode', function() + it('send_message converts boolean parse_mode to MarkdownV2', function() api.send_message(123, 'Hello', { parse_mode = true }) local req = api._last_request() assert.equals('MarkdownV2', req.parameters.parse_mode) @@ -45,11 +45,59 @@ describe('methods', function() assert.is_table(decoded.inline_keyboard) end) + it('send_message JSON-encodes link_preview_options', function() + api.send_message(123, 'Hello', { + link_preview_options = { is_disabled = true } + }) + local req = api._last_request() + local decoded = json.decode(req.parameters.link_preview_options) + assert.is_true(decoded.is_disabled) + end) + + it('send_message JSON-encodes reply_parameters', function() + api.send_message(123, 'Hello', { + reply_parameters = api.reply_parameters(42, 123, true) + }) + local req = api._last_request() + local decoded = json.decode(req.parameters.reply_parameters) + assert.equals(42, decoded.message_id) + end) + + it('send_message JSON-encodes entities', function() + local entities = {{ type = 'bold', offset = 0, length = 5 }} + api.send_message(123, 'Hello', { entities = entities }) + local req = api._last_request() + local decoded = json.decode(req.parameters.entities) + assert.equals('bold', decoded[1].type) + end) + + it('send_message passes business_connection_id', function() + api.send_message(123, 'Hello', { business_connection_id = 'biz_123' }) + local req = api._last_request() + assert.equals('biz_123', req.parameters.business_connection_id) + end) + + it('send_message passes message_effect_id', function() + api.send_message(123, 'Hello', { message_effect_id = 'effect_1' }) + local req = api._last_request() + assert.equals('effect_1', req.parameters.message_effect_id) + end) + it('send_reply validates message table', function() local result = api.send_reply('not a table', 'text') assert.is_false(result) end) + it('send_reply rejects table without chat', function() + local result = api.send_reply({ message_id = 42 }, 'text') + assert.is_false(result) + end) + + it('send_reply rejects table without message_id', function() + local result = api.send_reply({ chat = { id = 123 } }, 'text') + assert.is_false(result) + end) + it('send_reply creates reply_parameters', function() api.send_reply({ chat = { id = 123 }, message_id = 42 }, 'Hello') local req = api._last_request() @@ -58,6 +106,28 @@ describe('methods', function() assert.equals(42, rp.message_id) end) + it('send_reply uses MarkdownV2 for boolean parse_mode', function() + api.send_reply( + { chat = { id = 123 }, message_id = 42 }, + 'Hello', + { parse_mode = true } + ) + local req = api._last_request() + assert.equals('MarkdownV2', req.parameters.parse_mode) + end) + + it('send_reply preserves custom reply_parameters', function() + local custom_rp = api.reply_parameters(99, 456, false) + api.send_reply( + { chat = { id = 123 }, message_id = 42 }, + 'Hello', + { reply_parameters = custom_rp } + ) + local req = api._last_request() + local rp = json.decode(req.parameters.reply_parameters) + assert.equals(99, rp.message_id) + end) + it('forward_message works', function() api.forward_message(123, 456, 789) local req = api._last_request() @@ -65,6 +135,14 @@ describe('methods', function() assert.equals(789, req.parameters.message_id) end) + it('forward_messages encodes message_ids', function() + api.forward_messages(123, 456, {1, 2, 3}) + local req = api._last_request() + assert.truthy(req.endpoint:find('/forwardMessages')) + local ids = json.decode(req.parameters.message_ids) + assert.equals(3, #ids) + end) + it('copy_message works', function() api.copy_message(123, 456, 789, { caption = 'New caption' }) local req = api._last_request() @@ -72,6 +150,14 @@ describe('methods', function() assert.equals('New caption', req.parameters.caption) end) + it('copy_messages works', function() + api.copy_messages(123, 456, {1, 2}) + local req = api._last_request() + assert.truthy(req.endpoint:find('/copyMessages')) + end) + + -- Media methods: file and opts handling + it('send_photo passes file param', function() api.send_photo(123, 'photo_file_id') local req = api._last_request() @@ -80,12 +166,150 @@ describe('methods', function() assert.equals('photo_file_id', req.file.photo) end) - it('send_document passes file param', function() - api.send_document(123, 'doc_id') + it('send_photo passes caption and opts', function() + api.send_photo(123, 'photo_file_id', { + caption = 'My photo', + parse_mode = 'HTML', + has_spoiler = true, + show_caption_above_media = true + }) + local req = api._last_request() + assert.equals('My photo', req.parameters.caption) + assert.equals('HTML', req.parameters.parse_mode) + assert.is_true(req.parameters.has_spoiler) + assert.is_true(req.parameters.show_caption_above_media) + end) + + it('send_audio passes file and thumbnail', function() + api.send_audio(123, 'audio_file_id', { thumbnail = 'thumb_id' }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendAudio')) + assert.equals('audio_file_id', req.file.audio) + assert.equals('thumb_id', req.file.thumbnail) + end) + + it('send_audio passes caption and metadata', function() + api.send_audio(123, 'audio_file_id', { + caption = 'My audio', + duration = 120, + performer = 'Artist', + title = 'Song' + }) + local req = api._last_request() + assert.equals('My audio', req.parameters.caption) + assert.equals(120, req.parameters.duration) + assert.equals('Artist', req.parameters.performer) + assert.equals('Song', req.parameters.title) + end) + + it('send_document passes file and thumbnail', function() + api.send_document(123, 'doc_file_id', { thumbnail = 'thumb_id' }) local req = api._last_request() assert.truthy(req.endpoint:find('/sendDocument')) + assert.equals('doc_file_id', req.file.document) + assert.equals('thumb_id', req.file.thumbnail) + end) + + it('send_document passes caption', function() + api.send_document(123, 'doc_file_id', { caption = 'My doc' }) + local req = api._last_request() + assert.equals('My doc', req.parameters.caption) + end) + + it('send_video passes file and thumbnail', function() + api.send_video(123, 'video_file_id', { thumbnail = 'thumb_id' }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendVideo')) + assert.equals('video_file_id', req.file.video) + assert.equals('thumb_id', req.file.thumbnail) + end) + + it('send_video passes caption and all opts', function() + api.send_video(123, 'video_file_id', { + caption = 'My video', + parse_mode = 'HTML', + duration = 60, + width = 1920, + height = 1080, + has_spoiler = true, + supports_streaming = true, + show_caption_above_media = true + }) + local req = api._last_request() + assert.equals('My video', req.parameters.caption) + assert.equals('HTML', req.parameters.parse_mode) + assert.equals(60, req.parameters.duration) + assert.equals(1920, req.parameters.width) + assert.equals(1080, req.parameters.height) + assert.is_true(req.parameters.has_spoiler) + assert.is_true(req.parameters.supports_streaming) + assert.is_true(req.parameters.show_caption_above_media) + end) + + it('send_video JSON-encodes caption_entities', function() + local entities = {{ type = 'bold', offset = 0, length = 5 }} + api.send_video(123, 'video_file_id', { caption_entities = entities }) + local req = api._last_request() + local decoded = json.decode(req.parameters.caption_entities) + assert.equals('bold', decoded[1].type) + end) + + it('send_animation passes file and thumbnail', function() + api.send_animation(123, 'anim_file_id', { thumbnail = 'thumb_id' }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendAnimation')) + assert.equals('anim_file_id', req.file.animation) + assert.equals('thumb_id', req.file.thumbnail) + end) + + it('send_animation passes caption', function() + api.send_animation(123, 'anim_file_id', { caption = 'My GIF' }) + local req = api._last_request() + assert.equals('My GIF', req.parameters.caption) + end) + + it('send_voice passes file param', function() + api.send_voice(123, 'voice_file_id') + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendVoice')) + assert.equals('voice_file_id', req.file.voice) + end) + + it('send_voice passes caption', function() + api.send_voice(123, 'voice_file_id', { caption = 'My voice' }) + local req = api._last_request() + assert.equals('My voice', req.parameters.caption) + end) + + it('send_video_note passes file and thumbnail', function() + api.send_video_note(123, 'vnote_file_id', { thumbnail = 'thumb_id' }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendVideoNote')) + assert.equals('vnote_file_id', req.file.video_note) + assert.equals('thumb_id', req.file.thumbnail) end) + it('send_video_note passes duration and length', function() + api.send_video_note(123, 'vnote_file_id', { duration = 30, length = 240 }) + local req = api._last_request() + assert.equals(30, req.parameters.duration) + assert.equals(240, req.parameters.length) + end) + + it('send_media_group encodes media', function() + local media = { + { type = 'photo', media = 'photo1' }, + { type = 'photo', media = 'photo2' } + } + api.send_media_group(123, media) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendMediaGroup')) + local decoded = json.decode(req.parameters.media) + assert.equals(2, #decoded) + end) + + -- Poll tests + it('send_poll encodes options', function() api.send_poll(123, 'Question?', { { text = 'Yes' }, { text = 'No' } @@ -96,6 +320,51 @@ describe('methods', function() assert.equals(2, #opts) end) + it('send_poll passes question_parse_mode', function() + api.send_poll(123, '*Bold question*', { + { text = 'Yes' }, { text = 'No' } + }, { question_parse_mode = 'MarkdownV2' }) + local req = api._last_request() + assert.equals('MarkdownV2', req.parameters.question_parse_mode) + end) + + it('send_poll JSON-encodes question_entities', function() + local entities = {{ type = 'bold', offset = 0, length = 4 }} + api.send_poll(123, 'Question?', { + { text = 'Yes' }, { text = 'No' } + }, { question_entities = entities }) + local req = api._last_request() + local decoded = json.decode(req.parameters.question_entities) + assert.equals('bold', decoded[1].type) + end) + + it('send_poll passes quiz options', function() + api.send_poll(123, 'Capital of France?', { + { text = 'Berlin' }, { text = 'Paris' }, { text = 'London' } + }, { + poll_type = 'quiz', + correct_option_id = 1, + explanation = 'Paris is the capital', + explanation_parse_mode = 'HTML' + }) + local req = api._last_request() + assert.equals('quiz', req.parameters.type) + assert.equals(1, req.parameters.correct_option_id) + assert.equals('Paris is the capital', req.parameters.explanation) + end) + + it('send_poll JSON-encodes explanation_entities', function() + local entities = {{ type = 'italic', offset = 0, length = 5 }} + api.send_poll(123, 'Q?', { + { text = 'A' }, { text = 'B' } + }, { explanation_entities = entities }) + local req = api._last_request() + local decoded = json.decode(req.parameters.explanation_entities) + assert.equals('italic', decoded[1].type) + end) + + -- Other send methods + it('send_dice works', function() api.send_dice(123, { emoji = '🎲' }) local req = api._last_request() @@ -109,6 +378,60 @@ describe('methods', function() assert.equals('typing', req.parameters.action) end) + it('send_location passes coordinates', function() + api.send_location(123, 51.5074, -0.1278) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendLocation')) + assert.equals(51.5074, req.parameters.latitude) + assert.equals(-0.1278, req.parameters.longitude) + end) + + it('send_location passes live period and opts', function() + api.send_location(123, 51.5074, -0.1278, { + live_period = 3600, + heading = 90, + proximity_alert_radius = 100 + }) + local req = api._last_request() + assert.equals(3600, req.parameters.live_period) + assert.equals(90, req.parameters.heading) + assert.equals(100, req.parameters.proximity_alert_radius) + end) + + it('send_venue passes all fields', function() + api.send_venue(123, 51.5074, -0.1278, 'Big Ben', 'Westminster, London', { + foursquare_id = 'fsq_123', + google_place_id = 'gp_123' + }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendVenue')) + assert.equals('Big Ben', req.parameters.title) + assert.equals('Westminster, London', req.parameters.address) + assert.equals('fsq_123', req.parameters.foursquare_id) + end) + + it('send_contact passes phone and name', function() + api.send_contact(123, '+447911123456', 'John', { + last_name = 'Doe', + vcard = 'BEGIN:VCARD' + }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendContact')) + assert.equals('+447911123456', req.parameters.phone_number) + assert.equals('John', req.parameters.first_name) + assert.equals('Doe', req.parameters.last_name) + end) + + it('set_message_reaction works', function() + api.set_message_reaction(123, 42, { + reaction = {{ type = 'emoji', emoji = '👍' }} + }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/setMessageReaction')) + local decoded = json.decode(req.parameters.reaction) + assert.equals('👍', decoded[1].emoji) + end) + it('send_paid_media works', function() api.send_paid_media(123, 100, {}) local req = api._last_request() @@ -116,6 +439,8 @@ describe('methods', function() assert.equals(100, req.parameters.star_count) end) + -- Edit methods + it('edit_message_text works', function() api.edit_message_text(123, 42, 'Updated', { parse_mode = 'HTML' }) local req = api._last_request() @@ -124,6 +449,67 @@ describe('methods', function() assert.equals('HTML', req.parameters.parse_mode) end) + it('edit_message_text converts boolean parse_mode', function() + api.edit_message_text(123, 42, 'Updated', { parse_mode = true }) + local req = api._last_request() + assert.equals('MarkdownV2', req.parameters.parse_mode) + end) + + it('edit_message_text passes inline_message_id', function() + api.edit_message_text(nil, nil, 'Updated', { inline_message_id = 'inline_123' }) + local req = api._last_request() + assert.equals('inline_123', req.parameters.inline_message_id) + end) + + it('edit_message_caption works', function() + api.edit_message_caption(123, 42, { + caption = 'New caption', + parse_mode = true + }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/editMessageCaption')) + assert.equals('New caption', req.parameters.caption) + assert.equals('MarkdownV2', req.parameters.parse_mode) + end) + + it('edit_message_media works', function() + local media = { type = 'photo', media = 'photo_id' } + api.edit_message_media(123, 42, media) + local req = api._last_request() + assert.truthy(req.endpoint:find('/editMessageMedia')) + local decoded = json.decode(req.parameters.media) + assert.equals('photo', decoded.type) + end) + + it('edit_message_reply_markup works', function() + local kb = api.inline_keyboard():row(api.row():callback_data_button('OK', 'ok')) + api.edit_message_reply_markup(123, 42, { reply_markup = kb }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/editMessageReplyMarkup')) + local decoded = json.decode(req.parameters.reply_markup) + assert.is_table(decoded.inline_keyboard) + end) + + it('edit_message_live_location works', function() + api.edit_message_live_location(123, 42, 52.0, 13.0, { heading = 180 }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/editMessageLiveLocation')) + assert.equals(52.0, req.parameters.latitude) + assert.equals(180, req.parameters.heading) + end) + + it('stop_message_live_location works', function() + api.stop_message_live_location(123, 42) + local req = api._last_request() + assert.truthy(req.endpoint:find('/stopMessageLiveLocation')) + end) + + it('stop_poll works', function() + api.stop_poll(123, 42) + local req = api._last_request() + assert.truthy(req.endpoint:find('/stopPoll')) + end) + it('delete_message works', function() api.delete_message(123, 42) local req = api._last_request() @@ -180,6 +566,36 @@ describe('methods', function() end) end) + describe('stickers', function() + it('send_sticker passes file param', function() + api.send_sticker(123, 'sticker_file_id') + local req = api._last_request() + assert.truthy(req.endpoint:find('/sendSticker')) + assert.equals('sticker_file_id', req.file.sticker) + end) + + it('send_sticker passes opts', function() + api.send_sticker(123, 'sticker_file_id', { emoji = '😀' }) + local req = api._last_request() + assert.equals('😀', req.parameters.emoji) + end) + + it('upload_sticker_file works', function() + api.upload_sticker_file(456, 'sticker_data', 'static') + local req = api._last_request() + assert.truthy(req.endpoint:find('/uploadStickerFile')) + assert.equals(456, req.parameters.user_id) + assert.equals('static', req.parameters.sticker_format) + end) + + it('set_sticker_set_thumbnail passes file', function() + api.set_sticker_set_thumbnail('my_set', 456, { thumbnail = 'thumb_data' }) + local req = api._last_request() + assert.truthy(req.endpoint:find('/setStickerSetThumbnail')) + assert.equals('thumb_data', req.file.thumbnail) + end) + end) + describe('inline', function() it('answer_callback_query works', function() api.answer_callback_query('123', { text = 'Hello!', show_alert = true }) diff --git a/spec/test_helper.lua b/spec/test_helper.lua index 5f75c5e..aa691b1 100644 --- a/spec/test_helper.lua +++ b/spec/test_helper.lua @@ -57,6 +57,7 @@ api.info.name = api.info.first_name -- Store request calls for assertions api._requests = {} local real_request = api.request +api._real_request = real_request api.request = function(endpoint, parameters, file) table.insert(api._requests, { endpoint = endpoint, diff --git a/src/builders.lua b/src/builders.lua index d7e725b..59fb9f4 100644 --- a/src/builders.lua +++ b/src/builders.lua @@ -345,7 +345,7 @@ return function(api) -- Input message content constructors function api.input_text_message_content(message_text, parse_mode, link_preview_options, encoded) - parse_mode = (type(parse_mode) == 'boolean' and parse_mode == true) and 'markdown' or parse_mode + parse_mode = (type(parse_mode) == 'boolean' and parse_mode == true) and 'MarkdownV2' or parse_mode local input_message_content = { ['message_text'] = tostring(message_text), ['parse_mode'] = parse_mode, diff --git a/src/init.lua b/src/init.lua index 8705020..4af71b6 100644 --- a/src/init.lua +++ b/src/init.lua @@ -20,7 +20,7 @@ local ltn12 = require('ltn12') local json = require('dkjson') local config = require('telegram-bot-lua.config') -api.version = '3.0-0' +api.version = '3.1-0' function api.configure(token, debug) if not token or type(token) ~= 'string' then @@ -59,21 +59,22 @@ function api.request(endpoint, parameters, file) local output = json.encode(safe, { ['indent'] = true }) print(output) end - if file and next(file) ~= nil then - local file_type, file_name = next(file) - if type(file_name) == 'string' then - local file_res = io.open(file_name, 'rb') - if file_res then - parameters[file_type] = { - filename = file_name, - data = file_res:read('*a') - } - file_res:close() + if file then + for file_type, file_name in pairs(file) do + if type(file_name) == 'string' then + local file_res = io.open(file_name, 'rb') + if file_res then + parameters[file_type] = { + filename = file_name, + data = file_res:read('*a') + } + file_res:close() + else + parameters[file_type] = file_name + end else parameters[file_type] = file_name end - else - parameters[file_type] = file_name end end parameters = next(parameters) == nil and {''} or parameters diff --git a/src/methods/inline.lua b/src/methods/inline.lua index 2cd6261..188d7d3 100644 --- a/src/methods/inline.lua +++ b/src/methods/inline.lua @@ -49,7 +49,7 @@ return function(api) function api.send_inline_article(inline_query_id, title, description, message_text, parse_mode, reply_markup) description = description or title message_text = message_text or description - parse_mode = (type(parse_mode) == 'boolean' and parse_mode == true) and 'markdown' or parse_mode + parse_mode = (type(parse_mode) == 'boolean' and parse_mode == true) and 'MarkdownV2' or parse_mode return api.answer_inline_query(inline_query_id, json.encode({{ ['type'] = 'article', ['id'] = '1', diff --git a/src/methods/messages.lua b/src/methods/messages.lua index ba0311f..ce1984a 100644 --- a/src/methods/messages.lua +++ b/src/methods/messages.lua @@ -41,7 +41,7 @@ return function(api) local reply_markup = opts.reply_markup reply_markup = type(reply_markup) == 'table' and json.encode(reply_markup) or reply_markup local parse_mode = opts.parse_mode - parse_mode = (type(parse_mode) == 'boolean' and parse_mode == true) and 'markdown' or parse_mode + parse_mode = (type(parse_mode) == 'boolean' and parse_mode == true) and 'MarkdownV2' or parse_mode local reply_parameters = opts.reply_parameters if not reply_parameters then reply_parameters = api.reply_parameters(message.message_id, message.chat.id, true) @@ -442,6 +442,8 @@ return function(api) function api.send_poll(chat_id, question, options, opts) opts = opts or {} options = type(options) == 'table' and json.encode(options) or options + local question_entities = opts.question_entities + question_entities = type(question_entities) == 'table' and json.encode(question_entities) or question_entities local explanation_entities = opts.explanation_entities explanation_entities = type(explanation_entities) == 'table' and json.encode(explanation_entities) or explanation_entities local reply_parameters = opts.reply_parameters @@ -452,6 +454,8 @@ return function(api) ['chat_id'] = chat_id, ['message_thread_id'] = opts.message_thread_id, ['question'] = question, + ['question_parse_mode'] = opts.question_parse_mode, + ['question_entities'] = question_entities, ['options'] = options, ['is_anonymous'] = opts.is_anonymous, ['type'] = opts.poll_type, diff --git a/telegram-bot-lua-3.1-0.rockspec b/telegram-bot-lua-3.1-0.rockspec new file mode 100644 index 0000000..3ed7721 --- /dev/null +++ b/telegram-bot-lua-3.1-0.rockspec @@ -0,0 +1,66 @@ +package = "telegram-bot-lua" +version = "3.1-0" +source = { + url = "git://github.com/wrxck/telegram-bot-lua.git", + dir = "telegram-bot-lua", + tag = "v3.1" +} +description = { + summary = "A feature-filled Telegram bot API library", + detailed = "A feature-filled Telegram bot API library written in Lua, with Bot API 9.4 support.", + homepage = "https://github.com/wrxck/telegram-bot-lua", + maintainer = "Matthew Hesketh ", + license = "GPL-3" +} +supported_platforms = { + "linux", + "macosx", + "unix", + "bsd" +} +dependencies = { + "lua >= 5.1", + "dkjson >= 2.5-2", + "luasec >= 0.6-1", + "luasocket >= 3.0rc1-2", + "multipart-post >= 1.1-1", + "luautf8 >= 0.1.1-1", + "copas >= 4.0" +} +build = { + type = "builtin", + modules = { + ["telegram-bot-lua"] = "src/init.lua", + ["telegram-bot-lua.config"] = "src/config.lua", + ["telegram-bot-lua.handlers"] = "src/handlers.lua", + ["telegram-bot-lua.builders"] = "src/builders.lua", + ["telegram-bot-lua.helpers"] = "src/helpers.lua", + ["telegram-bot-lua.tools"] = "src/tools.lua", + ["telegram-bot-lua.utils"] = "src/utils.lua", + ["telegram-bot-lua.compat"] = "src/compat.lua", + ["telegram-bot-lua.core"] = "src/core.lua", + ["telegram-bot-lua.polyfill"] = "src/polyfill.lua", + ["telegram-bot-lua.async"] = "src/async.lua", + ["telegram-bot-lua.b64url"] = "src/b64url.lua", + ["telegram-bot-lua.methods.updates"] = "src/methods/updates.lua", + ["telegram-bot-lua.methods.messages"] = "src/methods/messages.lua", + ["telegram-bot-lua.methods.chat"] = "src/methods/chat.lua", + ["telegram-bot-lua.methods.members"] = "src/methods/members.lua", + ["telegram-bot-lua.methods.forum"] = "src/methods/forum.lua", + ["telegram-bot-lua.methods.stickers"] = "src/methods/stickers.lua", + ["telegram-bot-lua.methods.inline"] = "src/methods/inline.lua", + ["telegram-bot-lua.methods.payments"] = "src/methods/payments.lua", + ["telegram-bot-lua.methods.games"] = "src/methods/games.lua", + ["telegram-bot-lua.methods.passport"] = "src/methods/passport.lua", + ["telegram-bot-lua.methods.bot"] = "src/methods/bot.lua", + ["telegram-bot-lua.methods.gifts"] = "src/methods/gifts.lua", + ["telegram-bot-lua.methods.checklists"] = "src/methods/checklists.lua", + ["telegram-bot-lua.methods.stories"] = "src/methods/stories.lua", + ["telegram-bot-lua.methods.suggested_posts"] = "src/methods/suggested_posts.lua", + ["telegram-bot-lua.adapters"] = "src/adapters/init.lua", + ["telegram-bot-lua.adapters.db"] = "src/adapters/db.lua", + ["telegram-bot-lua.adapters.redis"] = "src/adapters/redis.lua", + ["telegram-bot-lua.adapters.llm"] = "src/adapters/llm.lua", + ["telegram-bot-lua.adapters.email"] = "src/adapters/email.lua" + } +}