Canales y mensajes

Por: Artiko
buzznostrcanalesmensajesnip-29cli

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):

ValorQué es¿Lo creas tú?
streamStream lineal de mensajes. Es el tipo por defecto.
forumDiscusión estilo foro, con hilos.
dmConversación directa.No a mano: lo crea buzz dms open
workflowCanal 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:

ValorDefinición
openBuscable; cualquiera puede unirse sin invitación.
privateOculto; 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:

FlagValoresNotas
--nametextoObligatorio
--typestream | forumObligatorio salvo que --template lo aporte
--visibilityopen | privateObligatorio salvo que --template lo aporte
--descriptiontextoOpcional
--ttlsegundosHace el canal efímero
--templatenombre de plantillaPlantillas 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:

  • join solo funciona en canales open. 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, leave falla con 400 "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:

CampoCómo se fijaQuién puede
namechannels update --nameowner / admin
about (descripción)channels update --descriptionowner / admin
topicchannels topic --topiccualquier miembro
purposechannels purpose --purposecualquier 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"
FlagQué hace
--channelUUID del canal. Obligatorio
--contentTexto con @menciones y markdown. - lee stdin
--kind9 (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-toEvent ID del padre; crea o continúa un hilo
--broadcastAñ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
--fileAdjunta un archivo. Repetible
--mentionPubkey 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 defaultTope duro
messages get50200
messages thread100500
messages search20100

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 devuelve too many unique message mentions (max 50).
  • Las menciones dentro de código se ignoran: el CLI aplica strip_code_regions antes de buscar, y un @variable no 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ónKindNombre interno
Crear canal9007KIND_NIP29_CREATE_GROUP
Nombre, descripción, topic, purpose, TTL, archivar9002KIND_NIP29_EDIT_METADATA
Añadir / quitar miembro9000 / 9001KIND_NIP29_PUT_USER / REMOVE_USER
Unirse / salir9021 / 9022KIND_NIP29_JOIN_REQUEST / LEAVE_REQUEST
Borrar canal9008KIND_NIP29_DELETE_GROUP
Enviar mensaje9KIND_STREAM_MESSAGE
Editar mensaje40003KIND_STREAM_MESSAGE_EDIT
Borrar mensaje9005KIND_NIP29_DELETE_EVENT
Enviar diff40008KIND_STREAM_MESSAGE_DIFF
Mensaje de sistema del canal40099KIND_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 stream y forum; dm y workflow los crea el sistema. La visibilidad solo admite open y private —en el escritorio open se muestra como Public—, y los defaults del relay son open y stream.
  • El nombre se canonicaliza sin # inicial y el ttl hace el canal efímero —Temporary en la interfaz.
  • channels list limita a 500 y channels search a 1000 eventos de metadatos. join solo funciona en canales open, 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/about exigen owner o admin, topic/purpose los cambia cualquier miembro.
  • Los mensajes son kind 9 con tag h obligatorio y tope de 65 536 bytes; --content - lee de stdin y es la vía segura para texto con metacaracteres.
  • messages get devuelve 50 con tope 200; thread 100 con tope 500; search 20 con tope 100. --format compact reduce cada mensaje a id, contenido y fecha.
  • Editar es kind 40003 y es solo-Buzz: ningún cliente NIP-29 estándar lo renderiza. Borrar es kind 9005; siempre puedes borrar lo tuyo.
  • Las menciones se vuelven tags p desde @nombre, URI nostr: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