# Ejemplo completo: termómetro

Una aplicación que publica lecturas en un canal, recibe la respuesta del equipo, verifica el aviso y contesta en el hilo sin entrar en bucle.

Este ejemplo junta todas las piezas en un caso real. «Termómetro» es una aplicación que publica la temperatura de la sala de servidores en un canal del Chat. Cuando alguien del equipo contesta, la aplicación recibe un aviso, comprueba que es auténtico y responde en el mismo hilo, sin contestarse nunca a sí misma ni a otras aplicaciones.

Necesitas una aplicación externa con su clave de API, los scopes `chat:messages:send`, `chat:messages:view` y `chat:events:subscribe`, una dirección de webhook con su secreto y el SDK de JavaScript o Python. Si te falta algo, empieza por [Autenticación en el chat](https://os.utilia.ai/docs/developers/chat-platform/authentication).

**El recorrido completo de Termómetro**

Participantes: Termómetro, UTILIA OS y Equipo.

1. Termómetro → UTILIA OS: `POST /api/external/v1/chat/channels/:channelId/messages`. Publica la lectura con su identidad.
2. UTILIA OS → Equipo: Mensaje en el canal. Aparece firmado por Termómetro y llega como aviso.
3. Equipo → UTILIA OS: Respuesta en el hilo. Alguien del equipo contesta.
4. UTILIA OS → Termómetro: Aviso CHAT_MESSAGE_CREATED. Firmado con HMAC; solo trae el messageId y el channelId.
5. Termómetro (por su cuenta): Verifica la firma del aviso y contesta 200 enseguida.
6. Termómetro → UTILIA OS: `GET /api/external/v1/chat/messages/:messageId`. Lee el mensaje; si el autor es una aplicación, se detiene.
7. Termómetro → UTILIA OS: `POST .../messages con parentMessageId`. Contesta en el mismo hilo.

1) **Da acceso a la aplicación en el canal**

   Un propietario o administrador del canal abre el panel del canal, pestaña «Apps», pulsa «Dar acceso a una app», elige Termómetro y marca «Leer los mensajes del canal» y «Escribir mensajes». Desde ese momento la aplicación es miembro del canal. Apunta el identificador del canal (`channelId`).
2) **Publica una lectura**

   **Node.js**

   ```javascript
   import { UtiliaSDK } from '@utilia-os/sdk-js';

   const sdk = new UtiliaSDK({
     baseURL: 'https://os.utilia.ai/api',
     apiKey: process.env.UTILIA_API_KEY, // utl_xxx
   });
   const CHANNEL_ID = process.env.CHANNEL_ID; // cnv_xxx

   async function publicarLectura(temperatura) {
     const mensaje = await sdk.chat.sendMessage(CHANNEL_ID, {
       content: `Temperatura de la sala de servidores: ${temperatura.toFixed(1)} °C`,
     });
     console.log('Publicado:', mensaje.id);
     return mensaje;
   }

   await publicarLectura(21.4);
   ```

   **Python**

   ```python
   import os
   from utilia_sdk import UtiliaSDKSync, SendChatMessageInput

   sdk = UtiliaSDKSync(
       base_url="https://os.utilia.ai/api",
       api_key=os.environ["UTILIA_API_KEY"],  # utl_xxx
   )
   CHANNEL_ID = os.environ["CHANNEL_ID"]  # cnv_xxx

   def publicar_lectura(temperatura: float):
       mensaje = sdk.chat.send_message(
           CHANNEL_ID,
           SendChatMessageInput(
               content=f"Temperatura de la sala de servidores: {temperatura:.1f} °C",
           ),
       )
       print("Publicado:", mensaje.id)
       return mensaje

   publicar_lectura(21.4)
   ```
3) **Recibe el aviso, verifícalo y contesta**

   Cuando alguien responde en el canal, UTILIA OS envía el aviso `CHAT_MESSAGE_CREATED` a la dirección de webhook de la aplicación. El receptor verifica la firma, lee el mensaje con `getMessage` y contesta en su hilo, solo si el autor no es una aplicación.

   **Node.js (Express)**

   ```javascript
   import express from 'express';

   const app = express();
   app.use(express.raw({ type: 'application/json' })); // cuerpo tal como llega

   app.post('/webhooks/chat', async (req, res) => {
     let aviso;
     try {
       aviso = sdk.webhooks.verify(req.body.toString('utf8'), req.headers, process.env.UTILIA_WEBHOOK_SECRET);
     } catch {
       return res.status(401).end(); // firma no válida o aviso caducado
     }

     res.status(200).end(); // contesta enseguida; el resto, después

     if (aviso.event === 'CHAT_MESSAGE_CREATED') {
       const mensaje = await sdk.chat.getMessage(aviso.data.messageId);
       if (mensaje.author.kind === 'APP') return; // nunca respondas a una aplicación

       await sdk.chat.sendMessage(mensaje.channelId, {
         content: 'Recibido. Vuelvo a leer el sensor y publico la lectura.',
         parentMessageId: mensaje.id,
       });
     }
   });

   app.listen(3000);
   ```

   **Python (Flask)**

   ```python
   from flask import Flask, request
   from utilia_sdk import SendChatMessageInput

   app = Flask(__name__)

   @app.post("/webhooks/chat")
   def recibir_aviso():
       cuerpo = request.get_data(as_text=True)  # tal como llega, sin reserializar
       try:
           aviso = sdk.webhooks.verify(cuerpo, request.headers, os.environ["UTILIA_WEBHOOK_SECRET"])
       except Exception:
           return "firma no válida", 401

       if aviso["event"] == "CHAT_MESSAGE_CREATED":
           mensaje = sdk.chat.get_message(aviso["data"]["messageId"])
           if mensaje.author.kind != "APP":  # nunca respondas a una aplicación
               sdk.chat.send_message(
                   mensaje.channel_id,
                   SendChatMessageInput(
                       content="Recibido. Vuelvo a leer el sensor y publico la lectura.",
                       parent_message_id=mensaje.id,
                   ),
               )

       return "ok", 200
   ```

## Antes de ponerlo en producción

- Lee la clave de API y el secreto del webhook de variables de entorno o de un gestor de secretos, nunca del código.
- Descarta los avisos repetidos guardando su `X-Utilia-Delivery-Id`, como en el receptor de [Webhooks de chat](https://os.utilia.ai/docs/developers/chat-platform/webhooks).
- No publiques más de 600 mensajes por hora: es el límite de cada aplicación.

## Siguiente paso

Si Termómetro debe avisar en un canal propio, puede crearlo e invitar a las personas de guardia con los [endpoints de canales](https://os.utilia.ai/docs/developers/chat-platform/channel-endpoints).
