Referência da API
Referência completa para todas as APIs turn.* disponíveis em Apps Lua.
APIs Principais
APIs para gerenciamento de apps e configuração.
turn.app
Gerencia a configuração do seu app, subscrições de campos de contato e mapeamentos de ativos. Esta é a nova API preferencial.
Gerenciamento de Configuração
get_config(): Retorna uma tabela de toda a configuração (sempre busca a mais recente do banco de dados).get_config_value(key): Retorna o valor para uma chave específica.update_config(updates): Mescla uma tabela de atualizações na configuração existente.set_config(new_config): Substitui toda a configuração.
-- Obtém configuração completa
local config = turn.app.get_config()
-- Obtém valor específico
local api_key = turn.app.get_config_value("api_key")
if not api_key then
turn.logger.error("A chave da API não está configurada!")
return false
end
-- Atualiza configuração (mesclagem)
turn.app.update_config({ last_sync = os.time() })
-- Substitui toda a configuração
turn.app.set_config({ api_key = "new_key", webhook_url = "https://example.com" })
Subscrições de Campos de Contato
get_contact_subscriptions(): Obtém a lista atual de campos subscritos.set_contact_subscriptions(fields): Define a lista de campos de contato para subscrever.fields(table): Um array Lua de nomes de campos (ex:{"name", "age"}).- Retorna
success, reason- verifique o sucesso antes de continuar.
-- Define subscrições na instalação ou mudança de configuração
if event == "install" or event == "config_changed" then
local success, reason = turn.app.set_contact_subscriptions({"name", "surname", "email"})
if not success then
turn.logger.error("Falha ao definir subscrições: " .. reason)
return false
end
end
-- Obtém subscrições atuais
local fields = turn.app.get_contact_subscriptions()
for _, field in ipairs(fields) do
turn.logger.info("Inscrito em: " .. field)
end
-- Cancela inscrição de todos os campos
turn.app.set_contact_subscriptions({})
Mapeamento de Ativos e Jornadas (Avançado)
Para apps que incluem ativos ou jornadas, essas funções mantêm mapeamentos de UUID:
set_asset_mapping(mapping): Mapeia placeholders de ativos para seus UUIDs instalados.get_asset_mapping(): Retorna a tabela de mapeamento de ativos atual.get_asset_uuid(placeholder): Obtém o UUID para um ativo específico.set_journey_mapping(mapping): Mapeia arquivos de jornada para seus UUIDs instalados.get_journey_mapping(): Retorna a tabela de mapeamento de jornada atual.
-- Durante a instalação, se seu app inclui ativos empacotados
turn.app.set_asset_mapping({
["logo.png"] = "uuid-1234-5678",
["template.html"] = "uuid-abcd-efgh"
})
-- Mais tarde, quando você precisar do ativo
local logo_uuid = turn.app.get_asset_uuid("logo.png")
turn.assets
Carrega arquivos estáticos (como templates ou imagens) de uma pasta assets/ no arquivo .zip do seu app.
list(directory_path): Lista arquivos em um diretório.directory_path(string, opcional): O caminho dentro deassets/.
exists(asset_path): Verifica se um arquivo existe.asset_path(string): O caminho completo para o ativo.
load(asset_path): Carrega o conteúdo de um ativo.asset_path(string): O caminho completo para o ativo.
local journey_files = turn.assets.list("journeys")
for _, filename in ipairs(journey_files) do
local content = turn.assets.load("journeys/" .. filename)
turn.logger.info("Template de jornada carregado: " .. filename)
end
turn.configuration
Esta API está obsoleta e será removida em uma versão futura. Por favor, use turn.app em vez disso. Todas as funções neste módulo registrarão um aviso nos logs do seu app.
turn.manifest
API padronizada para instalação e desinstalação de apps baseadas em manifest. Esta API lida com a instalação de campos de contato, ativos de mídia e jornadas definidos no arquivo manifest.json do seu app.
turn.manifest.install(manifest)
Instala um app baseado em seu manifest. O processo de instalação:
- Cria todos os campos de contato
- Instala todos os ativos de mídia e rastreia seus IDs
- Cria todas as jornadas em dois passos (criar desabilitadas, depois vincular e habilitar)
- Cria todos os templates (pulado se mesmo nome+idioma já existe)
- Cria todos os WhatsApp flows (pulado se PUBLISHED, atualizado se DRAFT)
Parâmetros:
manifest(table): A tabela de manifest (normalmente carregada demanifest.json)
Retorna: Uma tabela com resultados da instalação:
{
success = true|false,
app = {...}, -- Metadados do app do manifest
contact_fields = {
total = number,
created = number,
failed = {field_name1, field_name2, ...}
},
media_assets = {
total = number,
created = number,
failed = {filename1, filename2, ...}
},
journeys = {
total = number,
created = number,
failed = {journey_name1, journey_name2, ...}
},
templates = {
total = number,
created = number,
skipped = number, -- Templates que já existiam
failed = {"name:language", ...}
},
flows = {
total = number,
created = number,
updated = number, -- Flows DRAFT que foram atualizados
skipped = number, -- Flows PUBLISHED que já existiam
failed = {flow_name1, flow_name2, ...}
}
}
Estrutura do Manifest: Seu manifest.json pode incluir:
{
"app": {
"name": "my-app",
"version": "1.0.0"
},
"contact_fields": [
{
"type": "STRING",
"name": "customer_id",
"display": "ID do Cliente",
"private": false
}
],
"media_assets": [
{
"asset_path": "images/logo.png",
"filename": "logo.png",
"content_type": "image/png",
"description": "Logo da empresa"
}
],
"journeys": [
{
"name": "Jornada de Boas-vindas",
"file": "welcome.md",
"description": "Dá boas-vindas a novos usuários"
}
],
"templates": [
{
"name": "mensagem_boas_vindas",
"file": "mensagem_boas_vindas.json"
}
],
"flows": [
{
"name": "flow_cadastro",
"file": "cadastro.json",
"categories": ["SIGN_UP"],
"publish": false
}
]
}
Localização dos Arquivos de Assets: Cada tipo de asset é carregado de um subdiretório específico dentro de assets/:
| Tipo de Asset | Campo no Manifest | Localização do Arquivo |
|---|---|---|
| Ativos de mídia | asset_path: "images/logo.png" | assets/images/logo.png |
| Jornadas | file: "welcome.md" | assets/journeys/welcome.md |
| Templates | file: "mensagem_boas_vindas.json" | assets/templates/mensagem_boas_vindas.json |
| Flows | file: "cadastro.json" | assets/flows/cadastro.json |
Por exemplo, com esta estrutura de diretórios:
my_app/
└── assets/
├── manifest.json
├── images/
│ └── logo.png
├── journeys/
│ └── welcome.md
├── templates/
│ └── mensagem_boas_vindas.json
└── flows/
└── cadastro.json
Formato do Arquivo de Template: Cada arquivo JSON de template deve conter a definição completa do template:
{
"language": "pt_BR",
"category": "UTILITY",
"components": [
{ "type": "BODY", "text": "Olá {{1}}, bem-vindo ao nosso serviço!" }
]
}
Nota: O campo name do manifest tem precedência sobre qualquer nome no arquivo.
Substituição de Placeholders: Ao criar jornadas que referenciam outras jornadas ou ativos de mídia, use placeholders:
- Mídia:
asset:filename.png- Será substituído pelo external_id real - Jornadas:
journey:journey-file.md- Será substituído pelo UUID real da jornada
Exemplo de Uso:
function App.on_event(app, number, event, data)
if event == "install" then
-- Carrega manifest dos ativos
local manifest_json = turn.assets.load("manifest.json")
local manifest = turn.json.decode(manifest_json)
-- Instala tudo
local results = turn.manifest.install(manifest)
if results.success then
turn.logger.info("Instalação concluída com sucesso!")
turn.logger.info(string.format(
"Campos de contato: %d/%d criados",
results.contact_fields.created,
results.contact_fields.total
))
turn.logger.info(string.format(
"Ativos de mídia: %d/%d instalados",
results.media_assets.created,
results.media_assets.total
))
turn.logger.info(string.format(
"Jornadas: %d/%d criadas",
results.journeys.created,
results.journeys.total
))
else
turn.logger.error("Instalação concluída com erros")
if #results.contact_fields.failed > 0 then
turn.logger.error("Campos de contato falhados: " ..
table.concat(results.contact_fields.failed, ", "))
end
end
return results.success
end
end
turn.manifest.uninstall(manifest, options)
Desinstala um app baseado em seu manifest. Por padrão, isso realiza uma desinstalação segura que preserva dados do usuário (campos de contato).
Parâmetros:
manifest(table): A tabela de manifest (normalmente carregada demanifest.json)options(table, opcional): Opções de configuraçãoremove_contact_fields(boolean): Setrue, remove campos de contato (padrão:false)
Retorna: Uma tabela com resultados da desinstalação:
{
success = true|false,
app = {...}, -- Metadados do app do manifest
contact_fields = {
removed = number,
skipped = boolean, -- true se campos de contato foram preservados
failed = {field_name1, field_name2, ...}
},
media_assets = {
count = number,
removed = 0,
note = "Ativos de mídia são preservados e podem ser deletados manualmente se necessário"
},
journeys = {
removed = number,
failed = {journey_name1, journey_name2, ...}
},
templates = {
count = number,
removed = 0,
note = "Templates são preservados pois requerem aprovação da Meta e podem ser usados em outros lugares"
},
flows = {
count = number,
removed = 0,
note = "Flows são preservados pois não podem ser deletados via API e podem ser usados em outros lugares"
}
}
Exemplo de Uso:
function App.on_event(app, number, event, data)
if event == "uninstall" then
-- Carrega manifest dos ativos
local manifest_json = turn.assets.load("manifest.json")
local manifest = turn.json.decode(manifest_json)
-- Desinstalação segura (preserva campos de contato)
local results = turn.manifest.uninstall(manifest)
if results.success then
turn.logger.info("Desinstalação concluída com sucesso!")
turn.logger.info(string.format(
"Jornadas removidas: %d",
results.journeys.removed
))
if results.contact_fields.skipped then
turn.logger.info("Campos de contato: Preservados (dados do usuário retidos)")
end
else
turn.logger.error("Desinstalação concluída com erros")
end
return results.success
end
end
Desinstalação Completa (remove tudo incluindo dados do usuário):
-- Remove tudo, incluindo campos de contato
local results = turn.manifest.uninstall(manifest, {
remove_contact_fields = true
})
Notas Importantes:
- Por padrão, campos de contato são preservados para proteger dados do usuário
- Ativos de mídia são sempre preservados (podem ser reutilizados por outros apps)
- Templates são sempre preservados (requerem aprovação da Meta e podem ser usados por outros fluxos/apps)
- Jornadas são removidas usando um processo de duas fases (limpar conteúdo, depois deletar)
- Apenas jornadas que correspondem aos nomes do manifest são removidas
APIs de Comunicação
APIs para comunicação externa e interação com a plataforma.
turn.apps
Chama funções em outros Apps Lua instalados no mesmo número. Isso permite arquiteturas modulares de apps onde apps especializados podem expor funcionalidades reutilizáveis.
call(app_name, function_name, arguments): Chama uma função em outro app.app_name(string): O nome do app alvo (conforme definido em seu manifest).function_name(string): A função a chamar no app alvo.arguments(table): Argumentos para passar para a função. Deve ser uma tabela (use{}para nenhum argumento).- Retorna
success, result- um booleano indicando sucesso e o resultado da função ou mensagem de erro.
-- Chamar uma função em outro app com argumentos
local success, result = turn.apps.call("fhir_app", "create_appointment", {"2024-01-15", "10:00"})
if success then
turn.logger.info("Consulta criada: " .. tostring(result))
else
turn.logger.error("Falha ao criar consulta: " .. turn.json.encode(result))
end
-- Chamar sem argumentos (ainda deve passar tabela vazia)
local success, data = turn.apps.call("analytics_app", "get_daily_stats", {})
Tratando chamadas no app alvo:
O app alvo recebe chamadas entre apps como eventos journey_event com function_name e args nos dados:
function App.on_event(app, number, event, data)
if event == "journey_event" then
local fn = data.function_name
local args = data.args or {}
if fn == "create_appointment" then
local date, time = args[1], args[2]
-- Processa e retorna resultado
return "continue", { appointment_id = "APT-12345" }
elseif fn == "get_daily_stats" then
return "continue", { visits = 150, conversions = 23 }
end
end
end
Notas de Segurança:
- Apps só podem chamar outros apps instalados no mesmo número
- Chamadas entre números são negadas por razões de segurança
- Funções assíncronas (aquelas que retornam
wait) não podem ser chamadas de outros apps
turn.contacts
Encontra contatos e gerencia seus campos personalizados.
find(query): Encontra um contato.query(table): Pares chave-valor para buscar por (ex:{ msisdn = "+27..." }).
update_contact_details(contact, details): Atualiza os campos de um contato.contact(table): O objeto contato defind().details(table): Pares chave-valor de campos para atualizar.
create_contact_field(field_def): Cria um novo campo personalizado no esquema.field_def(table): Uma tabela com chavestype,nameedisplay.
local contact, found = turn.contacts.find({ msisdn = "+27820000000" })
if found then
turn.contacts.update_contact_details(contact, { loyalty_id = "LTY-12345" })
end
turn.http
Faz requisições HTTP externas.
request(options): Envia uma requisição HTTP.options(table): Uma tabela comurl,method,headers(table) ebody(string).
local body, status = turn.http.request({
url = "https://api.example.com/v1/events",
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = turn.json.encode({ message = "Olá" })
})
turn.journeys
Gerencia Jornadas programaticamente.
create(journey_def): Cria uma nova Jornada.journey_def(table): Uma tabela comname,notebookeenabled.
update(journey_uuid, updates): Atualiza uma Jornada existente.journey_uuid(string): O UUID da jornada para atualizar.updates(table): Uma tabela comname,notebookouenabled.
delete(journey_def): Deleta uma Jornada pelo nome.journey_def(table): Uma tabela com onameda jornada.
list(): Retorna uma lista de todas as Jornadas.
local journey, ok = turn.journeys.create({
name = "Integração de Novo Usuário",
notebook = turn.assets.load("journeys/onboarding.md"),
enabled = true
})
turn.templates
Cria e gerencia templates de mensagens do WhatsApp.
get(name, language): Obtém um template pelo nome e idioma.name(string): O nome do template.language(string): O código do idioma (ex:"en","pt_BR").
list(): Retorna uma lista de todos os templates para o número atual.exists(name, language): Verifica se um template existe.name(string): O nome do template.language(string): O código do idioma.- Retorna
truese o template existe,falsecaso contrário.
create(template_def): Cria um novo template.template_def(table): Uma tabela comname,language,categoryecomponents.- Retorna
template, success- o objeto do template e um booleano indicando sucesso. - Se um template com o mesmo nome+idioma já existe, retorna o template existente (idempotente).
- Novos templates começam com status
"PENDING"aguardando aprovação da Meta.
-- Verifica se o template existe
if not turn.templates.exists("mensagem_boas_vindas", "pt_BR") then
-- Cria um novo template
local template, success = turn.templates.create({
name = "mensagem_boas_vindas",
language = "pt_BR",
category = "UTILITY", -- AUTHENTICATION, MARKETING ou UTILITY
components = {
{ type = "HEADER", format = "TEXT", text = "Bem-vindo à {{1}}" },
{ type = "BODY", text = "Olá {{1}}, obrigado por se juntar!" },
{ type = "FOOTER", text = "Responda PARAR para cancelar" },
{ type = "BUTTONS", buttons = {
{ type = "QUICK_REPLY", text = "Começar" }
}}
}
})
if success then
turn.logger.info("Template criado: " .. template.name)
-- template.status será "PENDING" até a Meta aprovar
else
turn.logger.error("Falha ao criar template: " .. template)
end
end
-- Obtém um template existente
local template = turn.templates.get("mensagem_boas_vindas", "pt_BR")
-- Lista todos os templates
local templates = turn.templates.list()
for _, t in ipairs(templates) do
turn.logger.info(t.name .. " (" .. t.language .. "): " .. t.status)
end
Tipos de Componentes:
HEADER: Cabeçalho opcional comformat(TEXT,IMAGE,VIDEO,DOCUMENT,LOCATION) etextpara formato TEXTBODY: Corpo da mensagem obrigatório comtextcontendo o conteúdo principalFOOTER: Rodapé opcional comtextBUTTONS: Array de botões opcional comtype(QUICK_REPLY,URL,PHONE_NUMBER,COPY_CODE,FLOW,VOICE_CALL)
Categorias:
AUTHENTICATION: Para senhas únicas e verificaçãoMARKETING: Para conteúdo promocionalUTILITY: Para mensagens transacionais (confirmações, atualizações, etc.)
turn.flows
Cria e gerencia WhatsApp Flows para interações estruturadas.
get(name): Obtém um flow pelo nome.name(string): O nome do flow.- Retorna o objeto do flow ou
nilse não encontrado.
list(): Retorna uma lista de todos os flows para o WABA atual.exists(name): Verifica se um flow existe.name(string): O nome do flow.- Retorna
truese o flow existe,falsecaso contrário.
create(flow_def): Cria um novo flow.flow_def(table): Uma tabela comname,jsonecategoriesopcional.- Retorna
flow, success- o objeto do flow e um booleano indicando sucesso. - Se um flow com o mesmo nome já existe, retorna o flow existente (idempotente).
- Novos flows são criados com status
"DRAFT"e devem ser publicados via ferramentas da Meta.
-- Verifica se o flow existe
if not turn.flows.exists("flow_cadastro") then
-- Cria um novo flow
local flow, success = turn.flows.create({
name = "flow_cadastro",
json = '{"version":"5.0","screens":[...]}',
categories = { "SIGN_UP" } -- Opcional, padrão é { "OTHER" }
})
if success then
turn.logger.info("Flow criado: " .. flow.name .. " (ID: " .. flow.id .. ")")
-- flow.status será "DRAFT" - publique via ferramentas da Meta
else
turn.logger.error("Falha ao criar flow: " .. flow)
end
end
-- Obtém um flow existente
local flow = turn.flows.get("flow_cadastro")
-- Lista todos os flows
local flows = turn.flows.list()
for _, f in ipairs(flows) do
turn.logger.info(f.name .. ": " .. f.status)
end
Status dos Flows:
DRAFT: Flow está em desenvolvimento, pode ser atualizadoPUBLISHED: Flow está ativo e pode ser usado em conversasDEPRECATED: Flow foi marcado para aposentadoriaBLOCKED: Flow foi bloqueado pela MetaTHROTTLED: Flow está com taxa limitada
Categorias:
SIGN_UP,SIGN_IN,APPOINTMENT_BOOKING,LEAD_GENERATION,CONTACT_US,CUSTOMER_SUPPORT,SURVEY,OTHER
turn.leases
Usado para retomar Jornadas em espera. Veja a seção de fluxo assíncrono para um exemplo detalhado.
send_input(chat_uuid, input_data): Envia dados para uma Jornada pausada, retomando-a.chat_uuid(string): O UUID do chat cuja Jornada está aguardando.input_data(any): Os dados para enviar como resultado do blocoapp().
turn.leases.send_input(chat_uuid, { payment_status = "confirmed" })
Dados e Utilitários
APIs para processamento de dados, segurança e utilitários.
turn.json
Codifica e decodifica dados JSON.
encode(data, options): Codifica uma tabela Lua em uma string JSON.data(table): A tabela Lua para codificar.options(table, opcional): ex:{ indent = true }.
decode(json_string): Decodifica uma string JSON em uma tabela Lua.json_string(string): A string para decodificar.
local my_table = { name = "João Silva", age = 30 }
local json_string = turn.json.encode(my_table)
turn.encoding
Codifica e decodifica dados binários em vários formatos.
Codificação Base64
Codificação base64 padrão para incorporar dados binários em formatos JSON ou texto.
base64_encode(data): Codifica dados binários para string base64 com caracteres padrão (+, /) e preenchimento (=).base64_decode(encoded): Decodifica string base64 para dados binários. Gera erro em entrada inválida (capture compcall).
-- Codificar dados de imagem para JSON
local image_data = turn.assets.load("images/logo.png")
local base64_image = turn.encoding.base64_encode(image_data)
-- Enviar em requisição HTTP
turn.http.request({
url = "https://api.example.com/upload",
method = "POST",
body = turn.json.encode({ image = base64_image })
})
-- Decodificar com tratamento de erros
local ok, decoded = pcall(function()
return turn.encoding.base64_decode(encoded_data)
end)
if not ok then
turn.logger.error("Base64 inválido: " .. decoded)
end
Codificação Base64 Segura para URL
Variante segura para URL usando - e _ em vez de + e /, sem preenchimento. Ideal para tokens em URLs e parâmetros de consulta.
base64_url_encode(data): Codifica para base64 seguro para URL (sem preenchimento).base64_url_decode(encoded): Decodifica base64 seguro para URL. Gera erro em entrada inválida.
-- Criar token seguro para URL
local session_data = turn.json.encode({ user_id = 123, timestamp = os.time() })
local token = turn.encoding.base64_url_encode(session_data)
-- Usar em URL: https://example.com/verify?token=<token>
Codificação Hexadecimal
Converte dados binários de/para strings hexadecimais. Útil para exibir hashes e depuração.
hex_encode(data): Codifica dados binários para string hexadecimal em minúsculas.hex_decode(hex_string): Decodifica string hexadecimal para binário. Aceita maiúsculas e minúsculas. Gera erro em entrada inválida.
-- Exibir hash em formato hexadecimal
local hash = turn.crypto.sha256("dados importantes")
local hex_hash = turn.encoding.hex_encode(hash)
turn.logger.info("Hash dos dados: " .. hex_hash)
-- Decodificar hexadecimal
local binary = turn.encoding.hex_decode("48656c6c6f")
-- Retorna: "Hello"
turn.crypto
Operações criptográficas para hash seguro, HMAC e geração aleatória.
Todas as operações criptográficas usam o módulo :crypto do Erlang com algoritmos padrão da indústria. A verificação HMAC usa comparação de tempo constante para prevenir ataques de temporização.
Geração de Assinatura HMAC
Gera assinaturas HMAC para verificação de webhook e autenticação de API.
hmac_sha256(key, message): Retorna HMAC-SHA256 como binário.hmac_sha256_hex(key, message): Retorna HMAC-SHA256 como string hexadecimal.hmac_sha256_base64(key, message): Retorna HMAC-SHA256 como base64.hmac_sha512(key, message): Retorna HMAC-SHA512 como binário.hmac_sha512_hex(key, message): Retorna HMAC-SHA512 como string hexadecimal.hmac_sha512_base64(key, message): Retorna HMAC-SHA512 como base64.
-- Gerar assinatura de webhook
local secret = turn.app.get_config_value("webhook_secret")
local payload = turn.json.encode({ event = "payment.completed" })
local signature = turn.crypto.hmac_sha256_hex(secret, payload)
-- Enviar para serviço externo
turn.http.request({
url = "https://partner.com/webhook",
method = "POST",
headers = {
["X-Signature"] = signature
},
body = payload
})
Verificação de Assinatura HMAC
Verifica assinaturas de webhook de serviços externos (Stripe, GitHub, PayPal, etc.).
verify_hmac_sha256(key, message, expected_signature): Verifica assinatura HMAC-SHA256 usando comparação de tempo constante. Retornatruese válido,falsecaso contrário. Aceita assinatura como hexadecimal ou binário.
-- Verificar webhook do Stripe
function App.on_event(app, number, event, data)
if event == "http_request" then
local signature = data.headers["Stripe-Signature"]
local webhook_secret = turn.app.get_config_value("stripe_webhook_secret")
-- Verificação de tempo constante previne ataques de temporização
if not turn.crypto.verify_hmac_sha256(webhook_secret, data.body, signature) then
turn.logger.warn("Assinatura de webhook inválida")
return true, { status = 401, body = "Assinatura inválida" }
end
-- Processar webhook verificado
local event_data = turn.json.decode(data.body)
-- ... processar evento ...
return true, { status = 200, body = "OK" }
end
end
Hash Criptográfico
Gera hashes para integridade de dados e checksums.
sha256(data): Retorna hash SHA-256 como binário.sha256_hex(data): Retorna hash SHA-256 como string hexadecimal.md5(data): Retorna hash MD5 como binário.md5_hex(data): Retorna hash MD5 como string hexadecimal.
-- Gerar hash de conteúdo
local content = "Dados importantes para rastrear"
local hash = turn.crypto.sha256_hex(content)
turn.logger.info("Hash do conteúdo: " .. hash)
-- Armazenar hash para verificação posterior
turn.app.update_config({ last_sync_hash = hash })
Geração Aleatória Segura
Gera valores aleatórios criptograficamente seguros para tokens, IDs de sessão e nonces.
random_bytes(length): Gera bytes aleatórios (máx 1024). Usa:crypto.strong_rand_bytes/1.random_string(length): Gera string aleatória segura para URL (máx 1024). Perfeito para tokens.
-- Gerar token de sessão
local session_token = turn.crypto.random_string(32)
turn.app.update_config({ session_token = session_token })
-- Gerar chave de API
local api_key = turn.crypto.random_string(64)
-- Gerar bytes aleatórios para chave de criptografia
local encryption_key = turn.crypto.random_bytes(32)
local key_hex = turn.encoding.hex_encode(encryption_key)
Criptografia Autenticada AES-GCM
Criptografa e descriptografa dados sensíveis usando AES-256-GCM, fornecendo confidencialidade e integridade. Ideal para tokens de sessão, cookies e dados de configuração sensíveis.
aes_gcm_encrypt(plaintext, key): Criptografa dados com AES-256-GCM.plaintext(string): Os dados a serem criptografados.key(string): Deve ter exatamente 32 bytes (usesha256()para derivar de segredos).- Retorna: Texto cifrado codificado em Base64 contendo nonce, dados criptografados e tag de autenticação.
aes_gcm_encrypt(plaintext, key, aad): Criptografa com Dados Adicionais Autenticados.aad(string): Dados de contexto que devem corresponder durante a descriptografia (ex: ID do usuário).
aes_gcm_decrypt(ciphertext, key): Descriptografa dados criptografados com AES-256-GCM.ciphertext(string): Texto cifrado codificado em Base64 deaes_gcm_encrypt().key(string): Deve ter exatamente 32 bytes.- Retorna:
plaintext, nilem caso de sucesso, ounil, mensagem_de_erroem caso de falha.
aes_gcm_decrypt(ciphertext, key, aad): Descriptografa com verificação de AAD.
-- Derivar uma chave de 32 bytes de um segredo
local key = turn.crypto.sha256(turn.app.get_config_value("encryption_secret"))
-- Criptografar dados sensíveis
local encrypted = turn.crypto.aes_gcm_encrypt("dados secretos", key)
turn.logger.info("Criptografado: " .. encrypted)
-- Descriptografar dados
local plaintext, err = turn.crypto.aes_gcm_decrypt(encrypted, key)
if err then
turn.logger.error("Falha na descriptografia: " .. err)
else
turn.logger.info("Descriptografado: " .. plaintext)
end
-- Usando AAD para vincular texto cifrado ao contexto (ex: ID do usuário)
-- Isso previne ataques de replay de tokens entre diferentes usuários
local user_id = "user-123"
local session = turn.json.encode({ token = "abc", expires = os.time() + 3600 })
local encrypted_session = turn.crypto.aes_gcm_encrypt(session, key, user_id)
-- A descriptografia falhará se o AAD não corresponder
local data, err = turn.crypto.aes_gcm_decrypt(encrypted_session, key, user_id)
if err then
turn.logger.warn("Falha na verificação da sessão: " .. err)
return false
end
Notas de Segurança:
- Cada criptografia gera um nonce aleatório único (sem reutilização de nonce)
- GCM fornece confidencialidade e integridade (criptografia autenticada)
- A descriptografia falha se o texto cifrado for adulterado, a chave estiver errada ou o AAD não corresponder
- Use AAD para vincular o texto cifrado ao contexto quando apropriado (previne ataques de replay)
- Nunca codifique chaves diretamente - armazene segredos de forma segura na configuração do app
turn.i18n
Suporte à internacionalização (i18n) para apps multi-idioma usando arquivos PO. As traduções são carregadas de assets/locales/{locale}/LC_MESSAGES/{domain}.po no arquivo ZIP do seu app.
Funções de Tradução
t(msgid): Traduz uma string usando o locale padrão e o domínio "messages".t(msgid, opts): Traduz com opções:locale(string, opcional): Sobrescreve o locale de destinodomain(string, opcional): Usa um domínio de tradução específico (padrão: "messages")- Quaisquer outras chaves são usadas como bindings de interpolação para placeholders
%{key}
-- Tradução simples usando locale padrão
turn.i18n.t("Hello")
-- Sobrescreve o locale para esta chamada
turn.i18n.t("Hello", {locale = "spa"})
-- Usa um domínio específico
turn.i18n.t("Invalid input", {domain = "errors"})
-- Com bindings de interpolação
turn.i18n.t("Hello %{name}", {name = "Maria"})
-- Combinado: locale, domínio e bindings
turn.i18n.t("Welcome %{name}", {locale = "por_BR", name = "João"})
Tradutor Vinculado a Locale
for_locale(locale): Cria uma função tradutora vinculada a um locale específico. Recomendado para traduções por contato.for_locale(locale, opts): Cria um tradutor vinculado com domínio padrão:domain(string, opcional): Domínio de tradução padrão para este tradutor
-- Cria um tradutor vinculado ao idioma do contato
local t = turn.i18n.for_locale(contact.language)
t("Hello") -- Usa o locale vinculado
t("Hello %{name}", {name = "Maria"}) -- Com interpolação
t("Goodbye", {locale = "eng"}) -- Ainda pode sobrescrever por chamada
-- Com domínio vinculado para mensagens de erro
local t_errors = turn.i18n.for_locale(contact.language, {domain = "errors"})
t_errors("Invalid input") -- Usa locale vinculado e domínio "errors"
t_errors("Other", {domain = "messages"}) -- Pode sobrescrever domínio por chamada
Estrutura de Arquivos de Tradução
Os arquivos PO devem ser colocados em assets/locales/{locale}/LC_MESSAGES/{domain}.po:
assets/
└── locales/
├── eng/
│ └── LC_MESSAGES/
│ ├── messages.po # Domínio padrão
│ └── errors.po # Domínio personalizado
├── spa/
│ └── LC_MESSAGES/
│ ├── messages.po
│ └── errors.po
└── por_BR/
└── LC_MESSAGES/
├── messages.po
└── errors.po
Exemplo de Arquivo PO
Crie assets/locales/por_BR/LC_MESSAGES/messages.po:
# Traduções para Português (Brasil)
msgid ""
msgstr ""
"Language: por_BR\n"
msgid "Hello"
msgstr "Olá"
msgid "Hello %{name}"
msgstr "Olá %{name}"
msgid "Welcome to our service"
msgstr "Bem-vindo ao nosso serviço"
Configuração do Manifest
Defina o locale padrão no seu manifest.json:
{
"app": {
"name": "my-app",
"version": "1.0.0",
"default_locale": "eng"
}
}
Ordem de Resolução de Idioma
- Locale vinculado via
for_locale()(se usando tradutor vinculado) options.locale- Sobrescrita explícita por chamadaapp.default_locale- Do manifest.json"eng"- Fallback final
Normalização de Código de Locale
A API normaliza automaticamente os códigos de locale:
- Códigos ISO-639-1 de 2 letras são convertidos para ISO-639-3 de 3 letras (ex:
"en"→"eng","pt"→"por") - Variantes regionais são preservadas (ex:
"pt-BR"→"por_BR","pt_BR"→"por_BR")
turn.qrcode
Gera imagens de código QR.
generate(options): Cria um PNG de código QR.options(table): Uma tabela comdata(string) e chaves opcionais comofilename,color,image_data.
local qr_table, ok = turn.qrcode.generate({
data = "https://www.turn.io/",
color = "#8654CD" -- Roxo da Turn.io!
})
turn.media
Salva dados binários (como imagens ou documentos) como mídia que pode ser reutilizada em mensagens.
save(media_data): Salva dados binários como um item de mídia.media_data(table): Uma tabela comdata(string binária),filename(string) econtent_type(string).
local qr_table, ok = turn.qrcode.generate({ data = "..." })
if ok then
local saved, media_info = turn.media.save(qr_table)
end
turn.google
Autentica com APIs do Google.
get_access_token(service_account_json, scopes): Obtém um token OAuth2.service_account_json(string): O conteúdo JSON do arquivo de conta de serviço.scopes(table, opcional): Uma lista de escopos da API do Google.
local ok, token = turn.google.get_access_token(sa_json)
if ok then
-- Use token na requisição turn.http
end
turn.liquid
Renderiza templates Liquid armazenados no diretório assets/liquid/ do seu app. Suporta todos os filtros e tags Liquid padrão, além de filtros de tradução personalizados para i18n.
render(template_name, variables): Renderiza um template Liquid com variáveis.template_name(string): Caminho para o template relativo aassets/liquid/(ex: "welcome.liquid").variables(table): Variáveis para passar ao template.
render(template_name, variables, options): Renderiza com opções adicionais.options.locale(string, opcional): Define o locale de tradução para filtrost.options.strict_variables(boolean, opcional): Erro em variáveis indefinidas.
-- Renderizar um template com variáveis
local html = turn.liquid.render("welcome.liquid", {
name = "Alice",
message = "Bem-vindo ao nosso serviço!"
})
-- Renderizar com locale para traduções
local html = turn.liquid.render("welcome.liquid", {
name = contact.name
}, { locale = contact.language })
Estrutura de Templates
Armazene templates em assets/liquid/ dentro do ZIP do seu app:
my_app/
└── assets/
└── liquid/
├── welcome.liquid
├── emails/
│ └── receipt.liquid
└── partials/
├── header.liquid
└── footer.liquid
Filtros de Tradução (t e t_plural)
Templates Liquid suportam filtros de tradução que usam os arquivos PO do seu app.
Tradução Básica (t):
{% raw %}
<!-- Tradução simples -->
{{ "Hello" | t }}
<!-- Com bindings de interpolação -->
{{ "Hello %{name}" | t: name: user.name }}
<!-- Sobrescrever locale para este filtro -->
{{ "Hello" | t: locale: "spa" }}
<!-- Usar um domínio de tradução específico -->
{{ "Invalid input" | t: domain: "errors" }}
{% endraw %}
Pluralização (t_plural):
O filtro t_plural trata formas singular/plural baseado em uma contagem:
{% raw %}
<!-- Plural básico: mostra "1 item" ou "5 items" -->
{{ "One item" | t_plural: "%{count} items", count: cart.size }}
<!-- Com bindings adicionais -->
{{ "One message from %{sender}" | t_plural: "%{count} messages from %{sender}", count: messages.size, sender: contact.name }}
{% endraw %}
Ambas as formas são traduzidas usando seus arquivos PO. Para count=1, a forma singular é usada; caso contrário a forma plural é usada.
Ordem de Resolução de Locale:
- Argumento
locale:no filtro (maior prioridade) - Opção
localepassada pararender() default_localedo manifest.json do app"eng"(fallback final)
Incluindo Parciais
Use a tag render para incluir outros templates:
{% raw %}
<!-- Em page.liquid -->
<div class="page">
{% render 'partials/header' %}
<p>Conteúdo principal</p>
{% render 'partials/footer' %}
</div>
{% endraw %}
turn.logger
Escreve logs que são visíveis na UI da Turn.io para depuração.
debug(message),info(message),warning(message),error(message)message(string): A mensagem de log.
turn.logger.error("Falha ao conectar com o banco de dados: " .. err_msg)
API do Servidor HTTP
Construa interfaces web dentro do seu App Lua. O roteador fornece roteamento estilo Express com middleware, gerenciamento automático de sessão e proteção CSRF.
Início Rápido
Aqui está um exemplo mínimo funcional:
-- Criar um roteador
local router = turn.http.server.router.new("Meu App")
-- Definir rotas
router:get("/")(function(request, response)
response:html("<h1>Olá Mundo!</h1>")
end)
router:get("/api/status")(function(request, response)
response:json({ status = "ok" })
end)
-- Tratar requisições HTTP no seu app
function App.on_event(app, number, event, data)
if event == "http_request" then
return true, router:handle(data)
end
end
turn.http.server.router
Roteador estilo Express para tratar requisições HTTP com suporte a middleware.
Criando um Roteador
new(name): Cria uma nova instância de roteador.name(string): Nome do roteador para identificação.- Retorna um objeto roteador.
local router = turn.http.server.router.new("Meu App")
Configuração
router:config(options): Configura opções do roteador.session_secret(string): Habilita gerenciamento automático de sessão com esta chave de criptografia.session_cookie(string): Nome do cookie para sessões (padrão:"session").csrf_cookie(string): Nome do cookie para tokens CSRF.
router:config({
session_secret = turn.app.get_config_value("session_secret"),
session_cookie = "session"
})
Registro de Rotas
Rotas são registradas usando funções de método HTTP que retornam uma função aceitando um handler:
router:get(path)(handler): Registra rota GETrouter:post(path)(handler): Registra rota POSTrouter:put(path)(handler): Registra rota PUTrouter:patch(path)(handler): Registra rota PATCHrouter:delete(path)(handler): Registra rota DELETE
Parâmetros de caminho usam a sintaxe :param:
-- Rota simples
router:get("/status")(function(request, response)
response:json({ status = "ok" })
end)
-- Rota com parâmetros
router:get("/users/:id")(function(request, response)
local user_id = request.params.id
response:json({ user_id = user_id })
end)
-- Múltiplos parâmetros
router:get("/orgs/:org_id/users/:user_id")(function(request, response)
response:json({
org = request.params.org_id,
user = request.params.user_id
})
end)
-- POST com dados de formulário
router:post("/users")(function(request, response)
local name = request.form.name
response:set_status(201):json({ created = true, name = name })
end)
Objeto Request
Handlers de rota recebem um objeto request com:
method(string): Método HTTP (GET, POST, etc.)path(string): Caminho da requisiçãoparams(table): Parâmetros de caminho (ex::idvirarequest.params.id)query(table): Parâmetros de query stringform(table): Parâmetros de form/bodyheaders(table): Cabeçalhos da requisição como lista de tuplas{name, value}cookies(table): Cookies parseadossession(table): Dados de sessão (quandosession_secretestá configurado)csrf_token(string): Token CSRF para proteção de formuláriois_htmx(boolean):truese a requisição tem o cabeçalhoHX-Request: true
Objeto Response
Handlers de rota recebem um objeto response com métodos encadeáveis:
set_status(code): Define o código de status HTTPheader(name, value): Define um cabeçalho (substitui cabeçalho existente com mesmo nome)add_header(name, value): Adiciona um cabeçalho (permite duplicatas, ex: para Set-Cookie)json(data): Envia resposta JSON com content-type apropriadohtml(content): Envia resposta HTML com content-type apropriadotext(content): Envia resposta texto plano com content-type apropriadoredirect(url, status?): Redireciona para URL (padrão 302)- Métodos HTMX:
hx_trigger(),hx_redirect(),hx_refresh(), etc. (veja Suporte HTMX)
-- Resposta JSON
response:json({ message = "Sucesso" })
-- Resposta HTML
response:html("<h1>Olá!</h1>")
-- Resposta texto plano
response:text("OK")
-- Redirecionamento
response:redirect("/dashboard")
-- Status e cabeçalhos personalizados
response:set_status(201)
:header("X-Custom", "valor")
:json({ created = true })
Middleware
router:use(middleware): Registra middleware que executa antes dos handlers de rota.middleware(function): Função recebendo(request, response, next).- Chame
next(request, response)para continuar para o próximo middleware/handler. - Não chame
next()para interromper (ex: para falhas de autenticação).
-- Middleware de logging
router:use(function(request, response, next)
turn.logger.info(request.method .. " " .. request.path)
next(request, response)
end)
-- Middleware de autenticação
router:use(function(request, response, next)
if not request.session.user_id then
response:set_status(401):json({ error = "Não autorizado" })
return -- Não chamar next() para bloquear a requisição
end
next(request, response)
end)
-- Middleware de transformação de requisição
router:use(function(request, response, next)
-- Adicionar propriedades computadas
request.user = load_user(request.session.user_id)
next(request, response)
end)
Gerenciamento de Sessão
Quando você configura session_secret, o roteador automaticamente trata cookies de sessão criptografados:
local router = turn.http.server.router.new("Meu App")
router:config({
session_secret = turn.app.get_config_value("session_secret")
})
-- Agora use request.session como uma tabela normal
router:post("/login")(function(request, response)
-- Apenas atribua valores - o roteador trata a criptografia automaticamente
request.session.user_id = 123
request.session.role = "admin"
response:redirect("/dashboard")
end)
router:get("/dashboard")(function(request, response)
-- Leia valores diretamente
if not request.session.user_id then
response:redirect("/login")
return
end
response:json({ user_id = request.session.user_id })
end)
router:post("/logout")(function(request, response)
-- Limpar todos os dados da sessão
request.session:clear()
response:redirect("/")
end)
Como as sessões funcionam:
- As sessões são criptografadas usando AES-256-GCM e armazenadas em cookies
- O roteador rastreia modificações de forma transparente usando metatables Lua
- Quando você escreve em
request.session.foo, ele marca a sessão como modificada - Após seu handler completar, sessões modificadas são criptografadas e salvas em cookies
- Ler dados de sessão não dispara um salvamento - apenas modificações
Suporte HTMX
O roteador inclui suporte integrado para HTMX, facilitando a construção de UIs dinâmicas com HTML-over-the-wire.
Propriedades da Requisição:
request.is_htmx(boolean):truese a requisição inclui o cabeçalhoHX-Request: true
router:post("/update")(function(request, response)
-- Verifica se é uma requisição HTMX
if request.is_htmx then
-- Retorna apenas o fragmento atualizado
response:html("<div>Atualizado!</div>")
else
-- Página completa para requisições não-HTMX
response:redirect("/page")
end
end)
Métodos de Resposta HTMX:
Todos os métodos são encadeáveis e definem os cabeçalhos HTMX apropriados:
hx_trigger(event, detail?): Dispara um evento no lado do clientehx_trigger_after_settle(event, detail?): Dispara após o DOM estabilizarhx_trigger_after_swap(event, detail?): Dispara após a troca completarhx_redirect(url): Redirecionamento no lado do cliente (via cabeçalhoHX-Redirect)hx_refresh(): Atualização completa da páginahx_push_url(url): Adiciona URL ao histórico do navegadorhx_replace_url(url): Substitui URL no histórico do navegador (sem nova entrada)hx_reswap(method): Sobrescreve o método de swap (innerHTML,outerHTML, etc.)hx_retarget(selector): Sobrescreve o elemento alvohx_reselect(selector): Sobrescreve a seleção da resposta
-- Dispara um evento no lado do cliente com dados
router:post("/item/:id")(function(request, response)
local item = update_item(request.params.id, request.form)
response:hx_trigger("itemUpdated", {id = item.id})
:html("<div class='item'>" .. item.name .. "</div>")
end)
-- Redirecionamento com suporte a HTMX (lado do cliente, não 302)
router:post("/login")(function(request, response)
if authenticate(request.form) then
if request.is_htmx then
-- HTMX trata isso no lado do cliente
response:hx_redirect("/dashboard"):html("")
else
-- Redirecionamento HTTP padrão
response:redirect("/dashboard")
end
else
response:set_status(401):html("<div class='error'>Credenciais inválidas</div>")
end
end)
-- Atualiza histórico do navegador
router:get("/page/:num")(function(request, response)
local content = get_page_content(request.params.num)
response:hx_push_url("/page/" .. request.params.num)
:html(content)
end)
-- Muda como o conteúdo é trocado
router:delete("/item/:id")(function(request, response)
delete_item(request.params.id)
-- Remove o elemento do DOM
response:hx_reswap("delete"):html("")
end)
Diferença entre redirect() e hx_redirect():
response:redirect(url)envia redirecionamento HTTP 302 - navegador navega completamenteresponse:hx_redirect(url)define cabeçalhoHX-Redirect- HTMX trata a navegação no lado do cliente
Use hx_redirect() para requisições HTMX para evitar recarregamentos completos de página e manter as transições suaves do HTMX.
Tratando Requisições
router:handle(conn_data): Roteia uma requisição para o handler apropriado.conn_data(table): Dados de conexão do evento HTTP.- Retorna tabela de resposta com
status,headers,body. - Define automaticamente o cookie CSRF em todas as respostas.
function App.on_event(app, number, event, data)
if event == "http_request" then
return true, router:handle(data)
end
end
Respostas de Erro Integradas
O roteador trata automaticamente:
- 404 Not Found: Quando nenhuma rota corresponde ao caminho
- 405 Method Not Allowed: Quando o caminho corresponde mas o método não
Exemplo Completo
Aqui está um exemplo completo de uma aplicação web com autenticação, proteção CSRF e gerenciamento de sessão:
local server = require("turn.http.server")
local router = require("turn.http.server.router")
-- Criar roteador com suporte a sessão
local app = router.new("Gerenciamento de Usuários")
app:config({
session_secret = turn.app.get_config_value("session_secret")
})
-- Middleware de logging
app:use(function(request, response, next)
turn.logger.info(request.method .. " " .. request.path)
next(request, response)
end)
-- Rotas públicas
app:get("/")(function(request, response)
response:html(server.render(request, "home.liquid"))
end)
app:get("/login")(function(request, response)
response:html(server.render(request, "login.liquid"))
end)
app:post("/login")(function(request, response)
-- Validar CSRF
if not server.validate_csrf(request) then
response:set_status(403):json({ error = "Token CSRF inválido" })
return
end
local email = request.form.email
local password = request.form.password
-- Validar credenciais (simplificado)
local user = authenticate(email, password)
if user then
-- Apenas atribua à sessão - o roteador cuida do resto
request.session.user_id = user.id
request.session.email = user.email
response:redirect("/dashboard")
else
response:html(server.render(request, "login.liquid", {
error = "Credenciais inválidas"
}))
end
end)
-- Rotas protegidas
app:get("/dashboard")(function(request, response)
if not request.session.user_id then
response:redirect("/login")
return
end
response:html(server.render(request, "dashboard.liquid", {
user_id = request.session.user_id,
email = request.session.email
}))
end)
app:post("/logout")(function(request, response)
if server.validate_csrf(request) then
request.session:clear()
end
response:redirect("/")
end)
-- Tratar requisições HTTP
function App.on_event(app, number, event, data)
if event == "http_request" then
local result = app:handle(data)
return true, result
end
end
Utilitários turn.http.server
Utilitários de baixo nível para casos de uso avançados. A maioria dos desenvolvedores usará o roteador.
Utilitários de Cabeçalho
get_header(headers, name): Obtém o valor de um cabeçalho de uma lista de cabeçalhos (case-insensitive).headers(table): Cabeçalhos como lista de tuplas{name, value}.name(string): Nome do cabeçalho a encontrar.- Retorna o valor do cabeçalho ou
nil.
local content_type = turn.http.server.get_header(request.headers, "content-type")
local auth_token = turn.http.server.get_header(request.headers, "Authorization")
Proteção CSRF
O servidor usa o padrão de cookie de dupla submissão para proteção CSRF. O roteador define automaticamente o cookie CSRF, e você pode validar tokens nos seus handlers.
validate_csrf(request): Valida o token CSRF.request(table): O objeto de requisição parseado.- Retorna
truese válido,falsecaso contrário. - Verifica o token no cabeçalho
X-CSRF-Tokenou no campo de formulário_csrf_token.
router:post("/update")(function(request, response)
if not turn.http.server.validate_csrf(request) then
response:set_status(403):json({ error = "Token CSRF inválido" })
return
end
-- Processar a requisição...
end)
Renderização de Template
render(request, template_name, variables): Renderiza um template Liquid com injeção automática de CSRF.request(table): O objeto de requisição parseado.template_name(string): Caminho para o template relativo aassets/liquid/.variables(table, opcional): Variáveis para passar ao template.- Adiciona automaticamente
csrf_tokenecsrf_inputàs variáveis do template.
router:get("/form")(function(request, response)
response:html(turn.http.server.render(request, "form.liquid", {
user = current_user
}))
end)
{% raw %}
<!-- csrf_token e csrf_input estão disponíveis automaticamente -->
<form method="POST" action="/submit">
{{ csrf_input }}
<input type="text" name="name" value="{{ user.name }}">
<button type="submit">Salvar</button>
</form>
<!-- Para requisições AJAX -->
<script>
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-Token': '{{ csrf_token }}',
'Content-Type': 'application/json'
},
body: JSON.stringify({ ... })
});
</script>
{% endraw %}
Criptografia de Sessão (Baixo nível)
Para gerenciamento manual de sessão sem o roteador. A maioria dos casos de uso deve usar router:config({ session_secret = ... }).
-
encrypt_session(data, secret): Criptografa dados de sessão.data(table): Dados de sessão para criptografar.secret(string): Segredo de criptografia.- Retorna string criptografada codificada em base64.
-
decrypt_session(encrypted, secret): Descriptografa dados de sessão.encrypted(string): String de sessão criptografada.secret(string): Segredo de criptografia.- Retorna a tabela de dados descriptografada, ou tabela vazia em caso de falha.
Parsing de Requisição (Baixo nível)
Para gerenciamento manual de requisição sem o roteador.
-
parse_request(conn_data, options): Faz parsing dos dados brutos de conexão.- Retorna um objeto de requisição com
method,path,query,form,headers,cookies, etc.
- Retorna um objeto de requisição com
-
create_response(): Cria um construtor de resposta.- Retorna um objeto de resposta com
set_status(),json(),html(),redirect(), etc.
- Retorna um objeto de resposta com