Adapters
February 11, 2026 ยท View on GitHub
telegram-bot-lua v3.0 includes built-in adapters for databases, Redis, LLMs, and email. All adapters are async-first: they automatically use non-blocking I/O when running inside the copas event loop (the default for api.run()), and fall back to synchronous I/O when called outside it.
Database (api.db)
Supports SQLite (via lsqlite3) and PostgreSQL (via pgmoon). Install the driver for your database:
luarocks install lsqlite3 # SQLite
luarocks install pgmoon # PostgreSQL
Connecting
-- SQLite (in-memory)
local db = api.db.connect({ driver = 'sqlite', path = ':memory:' })
-- SQLite (file)
local db = api.db.connect({ driver = 'sqlite', path = '/path/to/bot.db' })
-- PostgreSQL
local db = api.db.connect({
driver = 'postgres',
host = '127.0.0.1',
port = 5432,
database = 'mybot',
user = 'botuser',
password = 'secret',
ssl = true, -- optional
})
Queries
-- Create table
db:execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, points INTEGER)')
-- Insert with parameters (? placeholders)
db:execute('INSERT INTO users (id, name, points) VALUES (?, ?, ?)', {123, 'Alice', 100})
-- Query returns array of row tables
local rows = db:query('SELECT * FROM users WHERE points > ?', {50})
for _, row in ipairs(rows) do
print(row.name, row.points)
end
-- Execute returns ok, changes_count
local ok, changes = db:execute('DELETE FROM users WHERE points = 0')
Transactions
-- Automatic: rolls back on error, commits on success
local ok, err = db:transaction(function(conn)
conn:execute('UPDATE users SET points = points - 10 WHERE id = ?', {sender_id})
conn:execute('UPDATE users SET points = points + 10 WHERE id = ?', {receiver_id})
end)
-- Manual
db:begin()
db:execute('INSERT INTO log VALUES (?)', {'event'})
db:commit() -- or db:rollback()
Connection Management
db:is_connected() -- true/false
db:close()
Redis (api.redis)
Lightweight Redis client using raw RESP protocol over sockets. No additional dependencies required.
Connecting
local redis = api.redis.connect({
host = '127.0.0.1', -- default
port = 6379, -- default
password = 'secret', -- optional
db = 0, -- optional, database number
timeout = 5, -- optional, connection timeout in seconds
})
String Commands
redis:set('key', 'value')
redis:set('key', 'value', { ex = 60 }) -- expires in 60 seconds
redis:set('key', 'value', { px = 5000 }) -- expires in 5000 milliseconds
local val = redis:get('key') -- returns string or nil
redis:del('key')
redis:exists('key') -- returns boolean
redis:incr('counter')
redis:decr('counter')
redis:incrby('counter', 5)
redis:append('key', ' more')
Hash Commands
redis:hset('user:123', 'name', 'Alice')
redis:hget('user:123', 'name') -- 'Alice'
redis:hgetall('user:123') -- { name = 'Alice', ... }
redis:hdel('user:123', 'name')
redis:hexists('user:123', 'name') -- boolean
redis:hincrby('user:123', 'score', 10)
redis:hkeys('user:123') -- array of field names
redis:hvals('user:123') -- array of values
redis:hlen('user:123') -- field count
List Commands
redis:lpush('queue', 'item')
redis:rpush('queue', 'item')
redis:lpop('queue')
redis:rpop('queue')
redis:lrange('queue', 0, -1) -- all items
redis:llen('queue')
Set Commands
redis:sadd('tags', 'lua')
redis:srem('tags', 'lua')
redis:smembers('tags') -- array of members
redis:sismember('tags', 'lua') -- boolean
redis:scard('tags') -- set size
Sorted Set Commands
redis:zadd('leaderboard', 100, 'Alice')
redis:zadd('leaderboard', 200, 'Bob')
redis:zscore('leaderboard', 'Alice') -- 100
redis:zrange('leaderboard', 0, -1) -- ordered members
redis:zrange('leaderboard', 0, -1, true) -- with scores
redis:zcard('leaderboard')
redis:zrem('leaderboard', 'Alice')
Key Commands
redis:expire('key', 120) -- set TTL in seconds
redis:pexpire('key', 5000) -- set TTL in milliseconds
redis:ttl('key') -- remaining TTL in seconds
redis:pttl('key') -- remaining TTL in milliseconds
redis:keys('user:*') -- matching keys (use sparingly)
redis:type('key') -- 'string', 'hash', 'list', etc.
redis:rename('old', 'new')
JSON Helpers
Convenience methods that serialize/deserialize Lua tables as JSON:
redis:jset('config', { theme = 'dark', lang = 'en' })
redis:jset('session', { user_id = 123 }, { ex = 3600 })
local config = redis:jget('config') -- { theme = 'dark', lang = 'en' }
Raw Commands
local result = redis:command('LPOS', 'mylist', 'needle')
Connection Management
redis:ping() -- 'PONG'
redis:is_connected() -- boolean
redis:close()
LLM (api.llm)
Unified interface for OpenAI and Anthropic (Claude) APIs. No additional dependencies; uses the built-in HTTP client.
Creating an Instance
-- OpenAI
local llm = api.llm.new({
provider = 'openai',
api_key = os.getenv('OPENAI_API_KEY'),
model = 'gpt-4o', -- default model
base_url = 'https://api.openai.com/v1', -- optional, for proxies
defaults = { -- optional default options
temperature = 0.7,
max_tokens = 1000,
},
})
-- Anthropic (Claude)
local llm = api.llm.new({
provider = 'anthropic',
api_key = os.getenv('ANTHROPIC_API_KEY'),
model = 'claude-sonnet-4-5-20250929',
defaults = { max_tokens = 2048 },
})
Chat
local result = llm:chat({
{ role = 'user', content = 'What is the capital of France?' }
})
print(result.content) -- 'The capital of France is Paris.'
print(result.finish_reason) -- 'stop' or 'end_turn'
print(result.usage.total_tokens)
With options:
local result = llm:chat({
{ role = 'user', content = 'Tell me a joke' }
}, {
temperature = 0.9,
max_tokens = 200,
system = 'You are a comedian.',
})
Multi-turn conversation:
local result = llm:chat({
{ role = 'system', content = 'You are a helpful assistant.' },
{ role = 'user', content = 'What is 2+2?' },
{ role = 'assistant', content = '4' },
{ role = 'user', content = 'And 3+3?' },
})
Complete (Shorthand)
local result = llm:complete('Translate "hello" to French')
print(result.content)
Embeddings (OpenAI only)
local result = llm:embed('Hello world')
print(#result.embeddings[1]) -- vector dimension
-- Batch embeddings
local result = llm:embed({'Hello', 'World'})
Error Handling
local result, err = llm:chat({{ role = 'user', content = 'test' }})
if not result then
print('LLM error:', err)
end
Example: AI-powered Bot
local api = require('telegram-bot-lua').configure(os.getenv('BOT_TOKEN'))
local llm = api.llm.new({
provider = 'anthropic',
api_key = os.getenv('ANTHROPIC_API_KEY'),
model = 'claude-sonnet-4-5-20250929',
})
function api.on_message(message)
if not message.text then return end
api.send_typing(message.chat.id)
local result, err = llm:chat({
{ role = 'user', content = message.text }
}, { system = 'You are a helpful Telegram bot. Keep answers concise.' })
if result then
api.send_message(message.chat.id, result.content)
else
api.send_message(message.chat.id, 'Sorry, I encountered an error.')
end
end
api.run({ timeout = 60 })
Email (api.email)
SMTP email sending via luasocket (already a dependency).
Creating an Instance
local mailer = api.email.new({
host = 'smtp.gmail.com',
port = 587, -- default
username = 'bot@gmail.com',
password = 'app-password',
tls = true, -- default, uses STARTTLS
domain = 'gmail.com', -- optional EHLO domain
})
Sending Email
-- Plain text
mailer:send({
from = 'bot@gmail.com',
to = 'user@example.com',
subject = 'Bot Notification',
body = 'Something happened in your bot!',
})
-- HTML
mailer:send({
from = 'bot@gmail.com',
to = 'user@example.com',
subject = 'Report',
html = '<h1>Daily Report</h1><p>All systems normal.</p>',
})
-- Both text and HTML (multipart/alternative)
mailer:send({
from = 'bot@gmail.com',
to = 'user@example.com',
subject = 'Report',
body = 'Daily Report\nAll systems normal.',
html = '<h1>Daily Report</h1><p>All systems normal.</p>',
})
-- Multiple recipients and CC
mailer:send({
from = 'bot@gmail.com',
to = { 'user1@example.com', 'user2@example.com' },
cc = 'admin@example.com',
reply_to = 'noreply@example.com',
from_name = 'My Bot',
subject = 'Notification',
body = 'Hello everyone.',
})
Convenience Methods
mailer:send_text('bot@gmail.com', 'user@example.com', 'Subject', 'Body')
mailer:send_html('bot@gmail.com', 'user@example.com', 'Subject', '<p>HTML</p>')
Error Handling
local ok, err = mailer:send({ ... })
if not ok then
print('Email failed:', err)
end
Async Behavior
All adapters automatically detect whether they're running inside the copas event loop:
- Inside
api.run()(default, async): adapters use non-blocking sockets and HTTP, allowing concurrent operations - Outside
api.run()or withapi.run({ sync = true }): adapters use standard blocking I/O
No code changes needed; the same adapter code works in both contexts.