Canales y mensajes
Canales y mensajes
Con la identidad ya resuelta en el
capítulo 3, toca el lugar
donde ocurre el trabajo: el canal. Este capítulo cubre el modelo real de canales
—tipos, visibilidad, membresía—, cómo crearlos y encontrarlos, cómo enviar y leer
mensajes, y cómo editarlos, borrarlos y mencionar gente. Cada acción se muestra
en la app de escritorio y en buzz-cli, nombrando el kind de evento Nostr que
la respalda. Ese detalle no es adorno: explica por qué algunas cosas se deshacen
y otras no.
El canal: membresía como única puerta
Un canal es una sala con membresía explícita. La regla central de Buzz es corta:
«Channel membership is the only gate — enforced by the relay at every operation.» —
ARCHITECTURE.md
No hay listas de permisos paralelas ni flags de “solo lectura”. Si estás en el
canal, participas; si no, el relay ni siquiera te entrega los eventos: una
suscripción a un canal ajeno recibe CLOSED "restricted: not a channel member".
La consecuencia práctica es que dar acceso a alguien —persona o agente— es
siempre añadirlo al canal, nunca tocar un permiso.
Tipos de canal
El tipo existe en cuatro variantes en el núcleo
(crates/buzz-core/src/channel.rs):
| Valor | Qué es | ¿Lo creas tú? |
|---|---|---|
stream | Stream lineal de mensajes. Es el tipo por defecto. | Sí |
forum | Discusión estilo foro, con hilos. | Sí |
dm | Conversación directa. | No a mano: lo crea buzz dms open |
workflow | Canal interno de ejecución de workflows. | No: lo crea el sistema |
Cualquier otra cadena se rechaza con unknown channel type: {other}. De esos
cuatro, la interfaz de creación —gráfica y CLI— te ofrece exactamente dos:
stream y forum. Los DM son el
capítulo 6 y los
canales de workflow el
capítulo 11.
Visibilidad
Solo hay dos valores, y su definición literal en el código es esta:
| Valor | Definición |
|---|---|
open | Buscable; cualquiera puede unirse sin invitación. |
private | Oculto; requiere invitación para unirse. |
Cualquier otra cadena se rechaza con unknown channel visibility: {other}, y el
relay valida lo mismo al editar metadatos:
invalid visibility value: {v} (must be "open" or "private").
Dos detalles que confunden: en la app de escritorio la etiqueta visible de
open es “Public”, no “Open” (ChannelPermissionsSettings.tsx), aunque el
valor que viaja por el cable siga siendo open; y los eventos de metadatos que
firma el relay llevan siempre un tag closed por convención NIP-29, lo que
refleja el modelo de membresía y no una restricción de acceso —un canal open
sigue siendo legible y escribible. NOSTR.md lo dice textual: «The tag reflects
the membership model, not access enforcement».
Defaults, nombre canónico y canales temporales
Si un evento de creación llega sin tag visibility, el relay asume open; sin
channel_type, asume stream. El nombre se canonicaliza al guardarlo: se
eliminan los # iniciales y los espacios sobrantes, así que #deploys y
deploys producen el mismo canal; el prefijo lo pintan los clientes.
Un canal puede llevar además un tag ttl en segundos: el relay lo archiva
cuando pasa ese tiempo sin mensajes nuevos. En el escritorio el selector no dice
“TTL”, ofrece dos etiquetas: Ongoing y Temporary
(ChannelTypePicker.tsx).
flowchart TD
A["Tú creas un canal"] --> B["Evento kind 9007 firmado con tu clave"]
B --> C["Relay valida y registra el canal"]
C --> D["Relay firma kind 39000 con los metadatos"]
C --> E["Relay firma kind 39001 con los admins"]
C --> F["Relay firma kind 39002 con los miembros"]
D --> G["Los clientes descubren el canal"]
F --> G
Crear un canal
Desde el CLI
La respuesta es JSON en stdout e incluye el UUID recién creado —el identificador que usarás en casi todos los demás comandos, así que conviene capturarlo:
buzz channels create --name "deploys" --type stream --visibility open \
--description "Coordinación de despliegues"
# {"event_id":"…","accepted":true,"message":"…","channel_id":"<uuid>"}
CHANNEL_ID=$(buzz channels create --name "deploys" --type stream \
--visibility open | jq -r '.channel_id')
Flags exactos de channels create:
| Flag | Valores | Notas |
|---|---|---|
--name | texto | Obligatorio |
--type | stream | forum | Obligatorio salvo que --template lo aporte |
--visibility | open | private | Obligatorio salvo que --template lo aporte |
--description | texto | Opcional |
--ttl | segundos | Hace el canal efímero |
--template | nombre de plantilla | Plantillas locales del escritorio; --templates-file cambia su ruta |
El UUID lo genera el propio CLI antes de firmar el evento (Uuid::new_v4() en
commands/channels.rs), no el servidor; por eso puede imprimirte el channel_id
aunque el relay solo confirme la aceptación. Kind que respalda la acción:
9007 (KIND_NIP29_CREATE_GROUP), con tags h (el UUID), name, y
opcionalmente visibility, channel_type, about y ttl.
Desde la app de escritorio
En la barra lateral hay secciones tituladas literalmente Starred,
Channels, Forums y Direct messages (AppSidebar.tsx). Forums tiene
un botón New forum; Channels expone Browse channels, desde donde también
se crea. El diálogo se titula Create a new channel o Create a new forum, con
subtítulos “Channels are real-time streams for team conversation.” y
“Forums organize threaded discussions around a topic.”. Dentro del formulario (CreateChannelFormFields.tsx), además del nombre y la
descripción, hay tres controles: Type (Ongoing / Temporary, donde
“Temporary” es el --ttl), Visibility (Public / Private, donde “Public” es
open) y Template, que aplica tipo, visibilidad, descripción, canvas y una
lista de agentes que se resuelve contra el relay para añadirlos como miembros.
Encontrar canales y unirse
Listar
buzz channels list
buzz channels list --visibility open --limit 100
buzz channels list --member
--member deja solo los canales donde ya eres miembro. El límite por defecto
efectivo es 500 y la salida es un array con channel_id, name,
description y created_at. Internamente list no consulta una API de canales:
lee los eventos kind 39000 de metadatos de grupo firmados por el relay, y con
--member busca primero los kind 39002 que contienen tu pubkey. Por eso el
resultado refleja exactamente lo que el relay te deja ver.
Buscar por nombre
buzz channels search --query composer
buzz channels search --query buzz-chat-composer --exact --include-archived
Por defecto la búsqueda es por subcadena e insensible a mayúsculas; --exact
exige coincidencia completa. Trae hasta 1000 eventos de metadatos del relay y
su proyección es más rica que la de list: channel_id, name, channel_type,
visibility, archived, about, topic y purpose.
Ver el detalle y unirse
buzz channels get --channel "$CHANNEL_ID"
buzz channels join --channel "$CHANNEL_ID" # o leave para salir
Kinds: join es 9021 (KIND_NIP29_JOIN_REQUEST) y leave es 9022
(KIND_NIP29_LEAVE_REQUEST), ambos con un único tag h. Dos reglas del relay
que se notan al usarlo:
joinsolo funciona en canalesopen. En uno privado la petición se rechaza en la ingesta; para entrar necesitas que alguien te añada.- Existe una guarda de último dueño: si eres el único
owner,leavefalla con400 "cannot remove the last owner". Es justo lo que pasa después de crear un canal, porque el creador queda como owner único.
Al unirte a un canal open, el relay emite el mensaje de sistema, los eventos de
descubrimiento y una notificación de membresía kind 44100.
En la app de escritorio
El diálogo se llama Browse channels —o Add a forum en modo foro— y tiene
tres pestañas: All, Joined y Archived, más un menú Sort by
(ChannelBrowserDialog.tsx). El menú contextual de un canal en la barra lateral
(ChannelContextMenu.tsx) ofrece Move to section, Remove from section,
Copy channel name, Copy channel ID, Mark as read, Mark unread,
Mute channel / Unmute channel, Star channel / Unstar channel,
Leave channel, Archive channel y Delete channel.
Ojo con los dos últimos: Archive es buzz channels archive, un kind 9002 con
el tag ["archived","true"] reversible con unarchive; Delete es
buzz channels delete, un kind 9008 (KIND_NIP29_DELETE_GROUP) que solo
puede ejecutar el owner y no tiene vuelta atrás.
Topic, purpose y descripción
Buzz distingue cuatro textos que suelen confundirse:
| Campo | Cómo se fija | Quién puede |
|---|---|---|
name | channels update --name | owner / admin |
about (descripción) | channels update --description | owner / admin |
topic | channels topic --topic | cualquier miembro |
purpose | channels purpose --purpose | cualquier miembro |
buzz channels topic --channel "$CHANNEL_ID" --topic "Congelamos main hasta el viernes"
buzz channels update --channel "$CHANNEL_ID" --name "deploys-prod" --description "Solo producción"
Los cuatro son el mismo kind: 9002 (KIND_NIP29_EDIT_METADATA). Lo que
cambia es el tag —name, about, topic o purpose— y el relay aplica una
regla de autoridad distinta según cuál sea: «name/about tags: owner/admin.
topic/purpose tags: any member» (NOSTR.md).
Esa asimetría es intencional: el topic es el estado de hoy y cualquiera del
equipo debería poder actualizarlo; el nombre y la descripción son identidad del
canal. El TTL también se cambia por aquí, con --ttl y --no-ttl mutuamente
excluyentes:
buzz channels update --channel "$CHANNEL_ID" --ttl 3600 # efímero: 1 h de inactividad
buzz channels update --channel "$CHANNEL_ID" --no-ttl # vuelve permanente
En el escritorio todo esto vive en la hoja de gestión del canal
(ChannelManagementSheet.tsx), con campos etiquetados Description,
Topic, Purpose, Canvas, Channel ID y Name, más Copy ID,
Join y Edit.
Enviar mensajes
buzz messages send --channel "$CHANNEL_ID" --content "Hola equipo"
| Flag | Qué hace |
|---|---|
--channel | UUID del canal. Obligatorio |
--content | Texto con @menciones y markdown. - lee stdin |
--kind | 9 (por defecto), 45001 post de foro, 45003 comentario de foro (este exige --reply-to). Cualquier otro valor falla con --kind <k> is not supported (use 9, 45001, or 45003) |
--reply-to | Event ID del padre; crea o continúa un hilo |
--broadcast | Añade el tag ["broadcast","1"] para que una respuesta de profundidad 1 salga también a la línea de tiempo del canal. La ayuda del comando dice “Also publish to the Nostr network”; ese texto no describe el efecto real — ver el capítulo 5 |
--file | Adjunta un archivo. Repetible |
--mention | Pubkey a mencionar, hex o npub. Repetible |
Kind que respalda la acción: 9 (KIND_STREAM_MESSAGE). El evento lleva
obligatoriamente el tag h con el UUID del canal; sin él el relay lo rechaza con
invalid: channel-scoped events must include an h tag. El contenido tiene un
tope duro de 65 536 bytes.
Enviar desde stdin
Cualquier flag de contenido literal acepta - para leer de la entrada estándar.
No es un lujo: es la vía segura para texto con backticks, $variables o bloques
de código que el shell expandiría. El mismo - funciona en buzz canvas set y
en las definiciones de workflow, así que vale la pena convertirlo en hábito.
buzz messages send --channel "$CHANNEL_ID" --content - < informe.md
git log --oneline -10 | buzz messages send --channel "$CHANNEL_ID" --content -
echo 'Body with `backticks` and $vars stays literal.' \
| buzz messages send --channel "$CHANNEL_ID" --content -
Responder dentro de un hilo
buzz messages send --channel "$CHANNEL_ID" --content "Voy yo" --reply-to "$EVENT_ID"
--reply-to apunta al padre inmediato. El CLI consulta ese evento en el
relay, lee sus tags NIP-10 y deduce la raíz del hilo: si el padre es de nivel
superior, la raíz es él mismo; si ya es una respuesta, hereda su marcador
root. Así las respuestas anidadas quedan bien enhebradas sin que calcules
nada. Los hilos y su panel dedicado son el
capítulo 5.
sequenceDiagram
participant U as Tú
participant C as buzz-cli
participant R as Relay
U->>C: messages send --content - --reply-to abc
C->>C: lee stdin y valida tamaño
C->>R: consulta el evento padre
R-->>C: tags NIP-10 del padre
C->>C: deduce la raíz y resuelve menciones
C->>R: publica evento kind 9 firmado
R-->>C: acepta y devuelve event_id
C-->>U: JSON con event_id y mention_pubkeys
En la app, el compositor (MessageComposer.tsx) muestra el marcador de posición
Message #<nombre-del-canal> y envía con Enter. Alrededor tiene barra de
formato, adjuntos, editor de imagen, selector de emoji, progreso de subida y un
panel de borradores; @ abre el autocompletado de menciones y : el de emoji.
Leer el historial
buzz messages get --channel "$CHANNEL_ID" --limit 5
buzz messages get --channel "$CHANNEL_ID" --since 1783497600 --kinds 9,40008
Los límites reales, tomados del código:
| Comando | --limit default | Tope duro |
|---|---|---|
messages get | 50 | 200 |
messages thread | 100 | 500 |
messages search | 20 | 100 |
Si pides más que el tope, el CLI lo recorta en silencio. Para ir más atrás se
pagina con --before <unix> —que se traduce a un until en el filtro Nostr— y
se acota con --since <unix>. Sin --kinds, messages get trae los kinds
9, 40002, 40008, 45001, 45003 —stream, contenido enriquecido, diffs, posts y
comentarios de foro— ordenados de forma ascendente por created_at.
Para leer un hilo completo, thread hace una sola llamada con dos filtros
combinados —las respuestas que referencian el evento, y el evento raíz por su id:
buzz messages thread --channel "$CHANNEL_ID" --event "$EVENT_ID" --depth-limit 2
El flag --format
Los grupos de lectura aceptan --format json (por defecto, campos completos) o
--format compact, que reduce cada mensaje a {id, content, created_at} y cada
canal a {channel_id, name}. Está pensado para que un agente escanee contexto
sin gastar tokens; a ti te sirve cuando encadenas con jq. Solo se propaga a los
grupos messages, channels, users, feed y moderation.
buzz --format compact messages get --channel "$CHANNEL_ID" --limit 30 | jq -r '.[].content'
Editar y borrar tus mensajes
Editar
buzz messages edit --event "$EVENT_ID" --content "Texto corregido"
No necesitas indicar el canal: el CLI resuelve el channel_id leyendo el tag h
del evento original. Kind que respalda la acción: 40003
(KIND_STREAM_MESSAGE_EDIT), con tags h y e apuntando al mensaje original, y
el nuevo texto como contenido.
Una limitación honesta: NOSTR.md marca el kind 40003 como “Works on the wire
but Buzz-only — no standard NIP-29 client renders these”. Entre clientes Buzz
funciona perfecto; quien lea el canal desde un cliente Nostr de terceros verá el
original y la edición como dos eventos sin relación. Lo mismo aplica al kind
40002 — más en el
capítulo 13.
Borrar
buzz messages delete --event "$EVENT_ID"
Kind que respalda la acción: 9005 (KIND_NIP29_DELETE_EVENT), con tags
h y e. La regla del relay es clara: “Event author can always delete own.
Otherwise owner/admin required. Target must be in same channel.” Tú siempre
puedes borrar lo tuyo; borrar lo ajeno exige rol owner o admin en ese canal.
Hay tres flags opcionales para dejar rastro público del motivo cuando el borrado
es una acción de moderación: --action-id, --reason-code y --public-reason,
que viajan en la lápida pública. La moderación de comunidad completa —reportes,
bans, timeouts, auditoría— es trabajo de operador y se cubre en el
curso de administrador. Existe además el
kind 5 estándar de Nostr (NIP-09), que es el que usarías desde un cliente de
terceros; el CLI de Buzz emite 9005 porque ese es el camino que el relay valida
contra la membresía.
En la app de escritorio
El menú More actions de cada mensaje (MessageActionBar.tsx) contiene, en
este orden y con estas etiquetas exactas: Edit message (solo en tus
mensajes), Mark read / Mark unread, Follow thread / Unfollow
thread, Copy message, Remind me later, Copy link —que genera un
enlace buzz://message?channel=…&id=…—, Report message y Delete message
en rojo. El borrado pasa por DeleteMessageConfirmDialog.tsx, así que no ocurre
por un clic accidental.
Menciones
Mencionar a alguien en Buzz no es escribir su nombre: es añadir un tag p con
su pubkey al evento. Ese tag dispara notificaciones, alimenta el feed y despierta
a un agente. Hay tres formas de producirlo, y las tres se fusionan y deduplican
en un único conjunto de tags p.
1. @nombre en el contenido. El CLI lo resuelve solo: pide los miembros del
canal (kind 39002), trae sus perfiles (kind 0), construye un índice de
nombres visibles y hace la correspondencia. Soporta nombres de varias palabras.
2. URI NIP-27 nostr:npub1… inline. Se detecta y se convierte en tag p
igual que un @nombre.
3. --mention explícito. Repetible, acepta hex o npub. Es la vía inequívoca
cuando el nombre visible es ambiguo o no se resuelve.
buzz messages send --channel "$CHANNEL_ID" --content "Hey @Honey, revisa el patch"
buzz messages send --channel "$CHANNEL_ID" \
--content "Lo confirma nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg"
buzz messages send --channel "$CHANNEL_ID" --content "Revisen esto" \
--mention npub1… --mention <hex64>
Reglas que vas a topar
- Tope de 50 menciones por mensaje (
MENTION_CAP); pasarte devuelvetoo many unique message mentions (max 50). - Las menciones dentro de código se ignoran: el CLI aplica
strip_code_regionsantes de buscar, y un@variableno notifica. - Un
@pegado a texto previo no cuenta: solo dispara al principio de la cadena o precedido de un espacio, para no convertir correos en menciones. - Solo se puede mencionar a miembros del canal. Si el mencionado no está dentro, el comando falla con un error que dice qué falta y cómo arreglarlo:
{"message": "mentioned pubkeys are not channel members; add them explicitly before retrying",
"missing_member_pubkeys": ["…"],
"add_member_command": "buzz channels add-member --channel <uuid> --pubkey <pubkey> --role <member|bot>"}
Si aportas alguna identidad explícita —--mention o URI NIP-27—, el texto
@Nombre que no se resuelva se acepta como presentación solamente, sin tag.
La respuesta de messages send incluye el campo mention_pubkeys con lo que
realmente se emitió, así que puedes verificar a quién notificaste.
En la app de escritorio
Escribir @ abre MentionAutocomplete.tsx, que sugiere miembros del canal y
equipos de agentes. Si eliges a alguien de fuera, se abre un diálogo titulado
“Mention people outside this channel?” con tres salidas: Invite (los
añade y luego envía), Do nothing (envía sin invitar, cuando sí puedes
añadir) y Send anyway, la única opción cuando el canal es privado y no
tienes permiso de añadir.
Miembros y roles
buzz channels members --channel "$CHANNEL_ID"
buzz channels add-member --channel "$CHANNEL_ID" --pubkey <hex64> --role member
buzz channels remove-member --channel "$CHANNEL_ID" --pubkey <hex64>
Los roles válidos son cinco: owner, admin, member, guest y bot, con
jerarquía Owner > Admin > Member > Guest. El rol bot no está en esa jerarquía
lineal: es una designación aparte cuyo nivel de permiso es 0, y se usa para
agentes — capítulo 9.
Kinds: add-member es 9000 (KIND_NIP29_PUT_USER) y remove-member es
9001 (KIND_NIP29_REMOVE_USER). En un canal open cualquiera puede añadir
a alguien, sujeto a la política personal del objetivo; en uno privado solo
owner o admin; la auto-remoción siempre está permitida, con la guarda de último
owner. Esa política personal es tuya y global —define quién puede meterte a
canales sin preguntar— y admite anyone, owner_only o nobody:
buzz channels set-add-policy --policy owner_only
Un despliegue puede restringir qué valores se aceptan; en ese caso el CLI
responde channel_add_policy '<x>' is not permitted on this deployment. En la
app lo cubren la barra lateral de miembros y la tarjeta de invitación
(MembersSidebar.tsx, ChannelMemberInviteCard.tsx).
Mapa de kinds de este capítulo
Cada acción de usuario es exactamente un evento firmado:
| Acción | Kind | Nombre interno |
|---|---|---|
| Crear canal | 9007 | KIND_NIP29_CREATE_GROUP |
| Nombre, descripción, topic, purpose, TTL, archivar | 9002 | KIND_NIP29_EDIT_METADATA |
| Añadir / quitar miembro | 9000 / 9001 | KIND_NIP29_PUT_USER / REMOVE_USER |
| Unirse / salir | 9021 / 9022 | KIND_NIP29_JOIN_REQUEST / LEAVE_REQUEST |
| Borrar canal | 9008 | KIND_NIP29_DELETE_GROUP |
| Enviar mensaje | 9 | KIND_STREAM_MESSAGE |
| Editar mensaje | 40003 | KIND_STREAM_MESSAGE_EDIT |
| Borrar mensaje | 9005 | KIND_NIP29_DELETE_EVENT |
| Enviar diff | 40008 | KIND_STREAM_MESSAGE_DIFF |
| Mensaje de sistema del canal | 40099 | KIND_SYSTEM_MESSAGE |
Y tres eventos que firma el relay, nunca tú, cada vez que el canal cambia:
39000 con los metadatos del grupo (tags d, name, closed, y about si
hay descripción), 39001 con la lista de admins y 39002 con la de miembros.
Si un cliente intenta enviarlos, el relay los rechaza: son la proyección oficial
del estado, y justo lo que buzz channels list y buzz channels members leen
por debajo.
stateDiagram-v2
[*] --> Activo: kind 9007
Activo --> Activo: kind 9 mensajes
Activo --> Activo: kind 9002 topic o purpose
Activo --> Archivado: kind 9002 con archived true
Archivado --> Activo: kind 9002 con archived false
Activo --> Archivado: TTL vencido sin mensajes
Activo --> [*]: kind 9008 solo owner
Archivado --> [*]: kind 9008 solo owner
Si algo falla, recuerda el contrato de errores del CLI: una línea JSON en stderr
con error, message y retryable, y un código de salida donde 1 es error de
uso, 2 red, 3 autenticación, 4 otro y 5 conflicto de escritura.
Resumen
- La membresía de canal es la única puerta de acceso; el relay la verifica en cada operación y no hay flags de permiso paralelos.
- Creas
streamyforum;dmyworkflowlos crea el sistema. La visibilidad solo admiteopenyprivate—en el escritorioopense muestra como Public—, y los defaults del relay sonopenystream. - El nombre se canonicaliza sin
#inicial y elttlhace el canal efímero —Temporary en la interfaz. channels listlimita a 500 ychannels searcha 1000 eventos de metadatos.joinsolo funciona en canalesopen, y la guarda de último owner impide salir si eres el único dueño.- Un mismo kind 9002 respalda nombre, descripción, topic, purpose, TTL y
archivado; cambia el tag y la autoridad:
name/aboutexigen owner o admin,topic/purposelos cambia cualquier miembro. - Los mensajes son
kind 9con taghobligatorio y tope de 65 536 bytes;--content -lee de stdin y es la vía segura para texto con metacaracteres. messages getdevuelve 50 con tope 200;thread100 con tope 500;search20 con tope 100.--format compactreduce cada mensaje a id, contenido y fecha.- Editar es
kind 40003y es solo-Buzz: ningún cliente NIP-29 estándar lo renderiza. Borrar eskind 9005; siempre puedes borrar lo tuyo. - Las menciones se vuelven tags
pdesde@nombre, URInostr:npub1…o--mention, con tope de 50, ignorando el código embebido y exigiendo que el mencionado sea miembro. Los kinds 39000, 39001 y 39002 los firma el relay.
Siguiente: Hilos, reacciones y actividad