O pacote telinha (sdk/python), versão 0.3. Tudo é asyncio: os métodos
marcados com await são corrotinas. Pra começar, veja o
tutorial.
from telinha import Bot, Client, Player, Reaction, Member, Message, Denied
from telinha import Embed, Button, Click, Icon, Style
from telinha import Screen, VideoPlayer
Client
Uma conexão com a sala, com o estado dela e os eventos. O Bot é um Client
com comandos; use o Client direto quando não precisar de comandos.
Client(invite, name, *, password=None, channel=None, channel_password="", bot_token="", key=None, reconnect=True)
| Parâmetro | |
|---|---|
invite |
o convite (telinha://end1,end2/canal!impressão#senha), ou só host:porta; também aceita um Invite |
name |
o nome na sala (até 32 bytes; repetido ganha (2)) |
password |
a senha da sala, se não vier no convite |
channel |
canal pra entrar, se não vier no convite (senão o padrão da sala) |
channel_password |
a senha desse canal, se ele tiver (a do join_channel/join_call o SDK lembra sozinho pra reconectar) |
bot_token |
o token de bot, se a sala pede (--token-bots) |
key |
número que identifica o bot pra banimento (padrão: sorteado a cada execução) |
reconnect |
reconectar sozinho quando a conexão cai (padrão True) |
Estado
| Atributo | |
|---|---|
me |
o id do bot na sala |
room |
o nome da sala |
welcome |
Welcome da última entrada |
members |
dict[int, Member]: quem o bot enxerga (o canal de texto dele, a call dele e os bots de qualquer canal) |
streaming |
o bot está transmitindo a tela |
channel |
o canal de texto atual |
call |
a call atual ("" = nenhuma) |
channels |
dict[str, ChannelInfo] |
room_locked |
a sala está trancada |
fingerprint |
a impressão da identidade do servidor (ou None, se ele não tiver) |
web_link |
o link pra assistir pelo navegador ("" = não tem) |
Métodos
| Método | |
|---|---|
run() |
roda até parar (Ctrl+C também); bloqueia |
await start() |
o mesmo, dentro de um programa asyncio; levanta Denied / WrongPassword |
await stop() |
sai da sala |
await wait_ready(timeout=None) |
espera o bot estar dentro da sala |
on(evento) |
decorador de handler: @client.on("message") |
off(evento, fn) |
tira um handler |
await send(texto, *, embed=None, image=None, reply_to=0, to=0, wait=True) |
manda no chat; reply_to cita uma mensagem, to manda em privado, embed junta um cartão, image uma imagem (arquivo, link ou ChatImage; precisa do ffmpeg). Retorna a Message registrada (ou None). Mais de 4000 bytes vira várias mensagens |
await edit(message_id, texto, *, embed=None) |
muda uma mensagem do próprio bot (o texto e o cartão; a imagem fica) |
await click(message_id, botão) |
clica num botão do cartão de outro bot |
await react(message_id, reação, on=True) |
põe (ou tira) uma reação |
await join_channel(nome, senha="") |
troca o canal de texto (cria se não existir e o servidor deixar) |
await join_call(nome, senha="") / await leave_call() |
entra na call de um canal / sai |
await set_voice(mic=None, deaf=None) |
microfone ligado (aparece na lista) / surdo |
await send_voice(opus) |
um pacote Opus (48 kHz) na call; prefira o Player |
await send_file(caminho, *, name=None, to=0, reply_to=0, progress=None) |
manda um arquivo (até 256 MB); retorna o id |
await download(file_info, destino) |
baixa um arquivo da sala (msg.file); destino pode ser uma pasta |
await start_stream(largura, altura, fps=30, *, audio=False) / await stop_stream() |
começa (ou muda o tamanho de) a transmissão de tela do bot / para; prefira Screen e VideoPlayer |
await send_frame(h264, *, key, width, height, droppable=False) |
um quadro H.264 (Annex B, um access unit) da transmissão |
await send_stream_audio(opus) |
um pacote Opus (48 kHz estéreo, 10 ms) do som da transmissão: vai só pra quem assiste |
member(id_ou_nome) |
acha alguém pelo id ou pelo nome (com ou sem @) |
room_time_us() |
agora, no relógio da sala |
Eventos
| Evento | Argumentos | Quando |
|---|---|---|
ready |
welcome: Welcome |
entrou na sala (de novo também, depois de reconectar) |
message |
msg: Message |
mensagem no chat do canal ou em privado, inclusive as do próprio bot e as antigas (msg.history) |
join |
member: Member |
apareceu alguém (no canal de texto ou na call do bot; bot, em qualquer canal) |
leave |
member: Member |
saiu |
members |
members: list[Member] |
a lista mudou (qualquer coisa: microfone, transmissão...) |
reaction |
message_id, member_id, reaction, on |
alguém pôs ou tirou uma reação |
action |
click: Click |
alguém clicou num botão de um cartão do bot (o on_click do botão também é chamado) |
edit |
message_id, text, embed |
uma mensagem mudou (um bot atualizando o cartão) |
channels |
current: str, channels: dict |
a lista de canais mudou (ou o bot trocou) |
notice |
kind, text, extra |
algo que um admin fez com o bot (Notice) |
voice |
member_id, seq, opus |
voz de alguém na call (pacote Opus cru) |
web_link |
url |
o link do navegador mudou |
keyreq |
kind |
a sala quer um quadro completo da transmissão do bot (alguém começou a assistir); a Screen responde sozinha |
pointer |
member_id, x, y, flags |
o ponteiro de quem assiste o bot: x, y de 0 a 1 no vídeo; flags & 2 = clique |
disconnect |
reason |
a conexão caiu (vai tentar de novo) |
Cada handler roda na sua própria tarefa; um erro nele vai pro log e não derruba o bot.
Bot
Bot(invite, name, *, prefix="!", help_command="ajuda", cooldown=1.0, accept_bots=False, lang="pt", **opções_do_Client)
| Parâmetro | |
|---|---|
prefix |
o que vem antes dos comandos |
help_command |
nome do comando de ajuda automático (None = sem; help também funciona) |
cooldown |
segundos entre comandos da mesma pessoa |
accept_bots |
obedecer comandos de outros bots |
lang |
idioma do que o SDK responde sozinho (uso errado, erro, ajuda): "pt" ou "en" |
@bot.command(name=None, *, help="", aliases=(), admin_only=False) registra
um comando. A função recebe ctx e os argumentos, convertidos pelos tipos
(int, float, str, bool, Member, *resto); o último str leva o
resto da linha. bot.commands tem todos.
Levantar CommandError("texto") dentro do comando responde o texto pra quem
chamou. Qualquer outro erro vai pro log e o bot responde "Deu erro aqui".
Context
ctx.bot |
o bot |
ctx.message |
a Message do comando |
ctx.command |
o Command |
ctx.args |
o texto depois do comando, cru |
ctx.author |
o Member que chamou (ou None se já saiu) |
await ctx.reply(texto) |
responde citando; retorna a Message |
await ctx.send(texto) |
manda no mesmo lugar (canal ou privada); retorna a Message |
await ctx.reply_card(embed, texto="", *, image=None) |
responde com um cartão, citando; texto é o que app de antes da 3.4 mostra no lugar |
Message
id |
o id na sala |
author_id, author |
quem mandou (0 = aviso do sistema) |
text |
o texto (Markdown do jeito que a pessoa escreveu) |
time_us |
a hora no relógio da sala |
history |
é antiga (chegou porque o bot acabou de entrar) |
system |
é aviso do sistema ("Ana entrou na sala") |
quote |
se é resposta: Quote(id, author_id, author, text) |
private, to_id, to_name |
privada, e pra quem |
file |
se tem arquivo: FileInfo(id, name, size) |
has_image |
tem imagem (o SDK não decodifica imagem) |
reactions |
[(reação, member_id)] (só nas antigas; as novas vêm pelo evento) |
channel |
o canal de texto do bot quando ela chegou |
embed |
o cartão (Embed), se tiver |
author_channel, author_call |
onde o autor está (canal de texto e call; "" = fora de call). O servidor só conta isso pra bot, e vale na privada também: é assim que o /msg Bot !join sabe pra onde ir |
await reply(texto) |
responde citando (em privado, se ela foi privada) |
await respond(texto) |
responde no mesmo lugar, sem citar |
await react(reação, on=True) |
|
await edit(texto=None, *, embed=None) |
muda a mensagem (só as do próprio bot) |
Member
id, name, mention |
mention = "@nome" |
bot |
é um bot |
owner, admin |
criou a sala / é admin |
streaming, camera |
transmitindo a tela / a câmera |
mic, deaf, muted |
microfone ligado / surdo / silenciado por um admin |
in_channel, in_call |
está no canal de texto do bot / na call do bot |
op, op_call |
é OP do canal de texto do bot / do canal da call do bot (quem criou também é) |
route |
por onde chegou (1 nessa máquina, 2 rede local, 3–4 internet, 5 link público, 6 navegador) |
version |
a versão do app dela |
watching |
as transmissões que ela está vendo |
ChannelInfo
name, people (no chat), in_call, topic, e live, fixed,
has_password, locked, hidden (invisível), op (o bot é OP dele),
created (foi o bot que criou).
Player
Player(client, *, bitrate=128_000, stereo=True, ffmpeg="ffmpeg")
await play(fonte, *, volume=None, start=0.0, headers=None) |
toca um arquivo ou link até o fim; retorna True se acabou, False se foi parado. Liga o microfone do bot enquanto toca. headers: cabeçalhos HTTP pro link (os que o yt-dlp manda usar) |
pause() / resume() |
|
await stop() |
para (o play retorna False) |
set_volume(0.0–2.0) |
vale em ~meio segundo, sem recomeçar a música (o ganho é aplicado nas amostras) |
playing, paused, position, source, volume |
Precisa do ffmpeg (FFmpegMissing se não tiver).
Transmitir a tela
O bot também transmite, como qualquer pessoa: quem está na mesma call clica
nele e assiste. Duas peças prontas, em telinha.video:
screen = Screen(bot)
await screen.show("placar.png") # arquivo, link, bytes, imagem do Pillow ou ChatImage
await screen.clear() # para de transmitir
player = VideoPlayer(bot, max_height=720)
await player.play("filme.mp4") # ou player.play(video_url, audio_url, headers=...)
Screen(client, *, max_side=1280): uma imagem parada (um painel, um
placar, a pergunta de um quiz). Cada show vira um quadro completo, e quem
começa a assistir depois recebe a última de novo: sem imagem nova, não sai
nada (quase zero de CPU e de banda). Junto com o evento pointer, dá pra fazer
uma tela que a galera clica.
VideoPlayer(client, *, max_height=720, bitrate=2_500_000, audio_bitrate=128_000):
await probe(fonte, headers=None) |
VideoInfo (codec, width, height, fps, duration, has_audio) pelo ffprobe |
await play(video, audio=None, *, headers=None, start=0.0, info=None) |
toca até o fim; True se acabou, False se foi parado. audio: o som num link separado (o YouTube manda assim). A transmissão continua ligada no fim, pra emendar a próxima |
pause() / resume() / await stop() |
pausar segura a imagem e o som juntos |
playing, paused, position, info, passthrough |
passthrough: o vídeo passa sem recodificar |
H.264 até max_height passa direto: os pacotes vão como vieram, sem
decodificar nem codificar de novo (o vídeo do YouTube em até 1080p é H.264).
Gasta quase nada de CPU; quadros B que ninguém usa de referência vão como
puláveis (a sala tira eles de quem não dá conta). Outro codec (VP9, AV1) o
ffmpeg recodifica pra H.264, e aí pesa (~1 núcleo pra 720p). O som vai como o
som da transmissão (Opus de 10 ms, no mesmo relógio da imagem), só pra quem
assiste. Quem assiste com app de antes da 3.6 sem decodificador na placa vê
vídeo com quadros B engasgando.
Embed
Um cartão no chat: barra colorida, uma linha pequena em cima (kicker),
título (link, com url), texto em Markdown, campos lado a lado, uma barra de
progresso que anda sozinha em cada app e até 5 botões. App de antes da 3.4
mostra só o texto da mensagem.
card = Embed(title="Popular Monster", kicker="NOW PLAYING", color=0xd946ef,
description="by **Falling In Reverse**", url="https://...",
progress=(31_000, 232_000), thumbnail=True, footer="SoundCloud")
card.add_field("Pedido por", "Ana").add_field("Depois", "Voices in My Head")
card.add_button("Pausar", icon=Icon.PAUSE, style="primary", on_click=pausar)
card.add_button("Pular", icon="next", on_click=pular)
msg = await bot.send("Tocando: Popular Monster", embed=card, image="capa.jpg")
...
card.kicker, card.paused = "PAUSADO", True
await msg.edit(embed=card)
title, url, kicker, description, footer |
texto (a descrição aceita **negrito**, *itálico*, `código` e [links](https://...)) |
color |
0xRRGGBB da barra (0 = a cor de destaque do tema de quem vê) |
thumbnail / big_image |
a imagem da mensagem vai como capa (no canto) / grande, embaixo |
progress, paused |
(posição_ms, total_ms); total 0 = ao vivo. Cada app anda sozinho até paused |
panel |
painel da transmissão do bot: quem assiste a tela dele (app 3.6+) vê o cartão encaixado embaixo do vídeo, com os botões; no chat ele continua igual |
add_field(nome, valor, inline=True) |
até 12 |
add_button(label="", *, icon=Icon.NONE, style="neutral", disabled=False, on_click=None) |
até 5. style: neutral, primary, danger, success. on_click: async def f(click: Click) |
Icon: PLAY, PAUSE, NEXT, PREVIOUS, STOP, SHUFFLE, REPEAT,
VOLUME_UP, VOLUME_DOWN, LIST, HEART, MUSIC, RADIO, SEARCH,
PLUS, CHECK, CLOSE, LINK, JOIN, TRASH, STAR, DICE (ou o nome
em texto: icon="next").
O clique chega só no autor do cartão, no máximo a cada 120 ms por pessoa, e edição no máximo a cada 200 ms (o SDK espera a vez, então duas edições seguidas chegam as duas). O SDK lembra os botões dos últimos 300 cartões.
Painel do bot
await bot.set_panel(embed) põe um cartão fixo do bot fora do chat: todo
mundo da sala vê de qualquer canal (no app, clicar no bot na lista abre os
controles, e o título aparece embaixo do nome dele). Os botões funcionam como
os de um cartão do chat; o clique chega com click.panel == True. Chame de
novo a cada mudança de estado (o SDK junta as seguidas e manda a última; o
servidor aceita uma a cada 200 ms em média) e set_panel(None) pra tirar. Ao
reconectar, o SDK manda o painel de novo. Servidor antes da 3.9 ignora. O bot
de música usa pra tocar, pausar, pular e chamar pra call de quem clicou.
Click
message_id, button |
qual cartão e qual botão (a partir de 0) |
member_id, member_name |
quem clicou |
channel, call |
onde a pessoa está |
panel |
o clique veio do painel do bot (set_panel), não de uma mensagem |
await reply(texto, **kw) |
responde citando o cartão (do painel: só pra quem clicou) |
await whisper(texto) |
responde só pra quem clicou (privada) |
Invite
Invite.parse(texto) → addresses (lista de Address(host, port)),
password, channel, fingerprint. str(invite) monta o convite de volta.
Erros
Denied |
a sala recusou ou tirou o bot; reason e code (Deny: KICKED, BANNED, CLOSED, LOCKED, FULL, BOTS...). Não reconecta |
WrongPassword |
senha errada |
IdentityMismatch |
o servidor não provou ser o do convite (impressão diferente) |
ConnectError |
não conectou (o motivo vem na mensagem) |
BadInvite |
convite sem endereço que dê pra usar |
CommandError |
dentro de um comando: responde o texto pra quem chamou |
Constantes
Reaction:LIKE,LOVE,HAHA,FIRE,WOW,SAD(as seis reações do app, nessa ordem).Notice:MUTED,UNMUTED,ADMIN_ON,ADMIN_OFF,CHANNEL,MOVED,DM,FILE_GONE...Deny: os motivos de recusa (acima).
O protocolo por baixo está em Protocolo.
