Antes de empezar debes preguntarte ¿por qué deberías aprender a realizar aplicaciones para MS Teams? Y te respondo, ya seas un desarrollador que trabaja en un ERP, CRM, una aplicación de monitorización, etc. Casi seguro que tendrás alguna información que vendría bien enviarla a algun canal de Teams: comunicar nuevas oportunidades, sacar un resumen las ventas, avisar de alguna incidencia del sistema, listado de mejoras de tu última versión (aquí lo enlazas con Azure DevOps), se me ocurren muchas cosas útiles y seguro que a ti más.

No tengo que decir que esta herramienta o similares llevan más de un año siendo la herramienta principal de trabajo. Es por así decirlo la herramienta mimada de cualquier fabricante, en este caso Teams para Microsoft, y desarrollar o implantar cualquier funcionalidad directamente relacionada con tu negocio incide directamente en la productividad.

Por tanto, si ¿alguna vez te has preguntado como realizar una aplicación en Microsoft Teams? si es así, continua leyendo. En 15 minutos tendrás tu prime Hello World! con Microsoft Teams. Eso sí, descontando el tiempo que nos lleve instalar las herramientas y cuentas necesarias.

Entra en los enlaces y sigue los siguientes pasos:

    1. Crea una cuenta de desarrolladores  en Microsoft 365
    2. Prepara el espacio empresarial de Microsoft 365
    3. Instala Git, NodeJS y Visual Studio Code
    4. En Visual Studio Code instala la extensión Preview: Team Toolkit
extension vs code teams

Entra en Quick Start, revisa los pasos y el video, aquí se explica todo muy bien, pero aun así, yo continuaré con el ejemplo:

Quick start teams

Creamos el primer proyecto de demostración, siguiendo los pasos que indico en la imagen:

Pasos para crear demo de Teams

Esperas el tiempo necesario para que pueda generar todo el proyecto.

Y lo ejecutamos directamente con las opciones de ejecución y depuración de VS Code. Automáticamente se han creado con la plantilla los ficheros launch.json y tasks.json. La primera vez que lo lances realizar cosa tales como la instalación de los NPM y la Azure App Application.

Ejecutar debug

Si lo deseas ya puedes poner una parada en el codigo, por ejemplo en:

Depurar App Teams

Y podrás ver el resultado en tu Teams, la instalas y ejecutas el botón:

Instalación y test

Desplegar y publicar. Para ello tienes que ejecutar los pasos que indica el Readme.md. Desde Visual Studio Code:

    1. Abrir Menú Ver >> Paleta de comandos…
    2. Hacer login en Azure, para ello escribes: Sign in to Azure.
    3. Generar recursos en Azure, en la paleta de comandos: Teams: Provisioning in the cloud.
    4. Desplegar: Teams: Deploy to cloud.

Con esta secciones lo que has realizado es desplegar la aplicación en Azure.

Ahora tienes que revisar que este todo bien:

    1. Run and Debug Activity Portal.
    2. Launch Remote (Edge)
    3. Ejecuta y podrás ver el correcto funcionamiento.

El ultimo paso es crear el zip para desplegar:

    1. En la paleta de comandos: Teams: Validate manifest.
    2. Y luego Zip Teams metada package.
    3. Con el zip lo podrás subir a teams.
Cargar zip generado

Enhorabuena has publicado tu primera aplicación en el tenant de producción (espero que sea de test) de tu organización.

Ahora toca personalizar un poco este proyecto y para ello realiza los mismos pasos, pero esta vez usa TypeScript en un proyecto llamado upload2Storage.

El primer paso será crea un Storages Account y obtener un SAS (shared access signature), como el siguiente.

Crear un token SAS

Debemos establecer CORS, de la siguiente forma:

Crear CORS

Si no te quedan claro los anteriores pasos, puedes seguir este enlace y ver las instrucciones más detallas. Tambien te servirá para lo que viene a continuación.

Debemos hacer una limpieza en el proyecto:

Nueva estrucuta

Añadimos una nueva entrada en los NPM del proyecto:

NPM nuevo

Y cambiamos el código de Tab.tsx:

// ./src/App.tsx

import React, { useState } from 'react';
import Path from 'path';
import uploadFileToBlob, { isStorageConfigured } from './azure-storage-blob';

const storageConfigured = isStorageConfigured();

const App = (): JSX.Element => {
  // all blobs in container
  const [blobList, setBlobList] = useState<string[]>([]);

  // current file to upload into container
  const [fileSelected, setFileSelected] = useState(null);

  // UI/form management
  const [uploading, setUploading] = useState(false);
  const [inputKey, setInputKey] = useState(Math.random().toString(36));

  const onFileChange = (event: any) => {
    // capture file into state
    setFileSelected(event.target.files[0]);
  };

  const onFileUpload = async () => {
    // prepare UI
    setUploading(true);

    // *** UPLOAD TO AZURE STORAGE ***
    const blobsInContainer: string[] = await uploadFileToBlob(fileSelected);

    // prepare UI for results
    setBlobList(blobsInContainer);

    // reset state/form
    setFileSelected(null);
    setUploading(false);
    setInputKey(Math.random().toString(36));
  };

  // display form
  const DisplayForm = () => (
    <div>
      <input type="file" onChange={onFileChange} key={inputKey || ''} />
      <button type="submit" onClick={onFileUpload}>
        Upload!
          </button>
    </div>
  )

  // display file name and image
  const DisplayImagesFromContainer = () => (
    <div>
      <h2>Container items</h2>
      <ul>
        {blobList.map((item) => {
          return (
            <li key={item}>
              <div>
                {Path.basename(item)}
                <br />
                <img src={item} alt={item} height="200" />
              </div>
            </li>
          );
        })}
      </ul>
    </div>
  );

  return (
    <div>
      <h1>Upload file to Azure Blob Storage</h1>
      {storageConfigured && !uploading && DisplayForm()}
      {storageConfigured && uploading && <div>Uploading</div>}
      <hr />
      {storageConfigured && blobList.length > 0 && DisplayImagesFromContainer()}
      {!storageConfigured && <div>Storage is not configured.</div>}
    </div>
  );
};

export default App;

Y añades codigo a azure-storage-blob.tsx:

// ./src/azure-storage-blob.ts

// <snippet_package>
// THIS IS SAMPLE CODE ONLY - NOT MEANT FOR PRODUCTION USE
import { BlobServiceClient, ContainerClient} from '@azure/storage-blob';

const containerName = `tutorial-container`;
const sasToken = process.env.REACT_APP_STORAGESASTOKEN;
const storageAccountName = process.env.REACT_APP_STORAGERESOURCENAME; 
// </snippet_package>

// <snippet_isStorageConfigured>
// Feature flag - disable storage feature to app if not configured
export const isStorageConfigured = () => {
  return (!storageAccountName || !sasToken) ? false : true;
}
// </snippet_isStorageConfigured>

// <snippet_getBlobsInContainer>
// return list of blobs in container to display
const getBlobsInContainer = async (containerClient: ContainerClient) => {
  const returnedBlobUrls: string[] = [];

  // get list of blobs in container
  // eslint-disable-next-line
  for await (const blob of containerClient.listBlobsFlat()) {
    // if image is public, just construct URL
    returnedBlobUrls.push(
      `https://${storageAccountName}.blob.core.windows.net/${containerName}/${blob.name}`
    );
  }

  return returnedBlobUrls;
}
// </snippet_getBlobsInContainer>

// <snippet_createBlobInContainer>
const createBlobInContainer = async (containerClient: ContainerClient, file: File) => {
  
  // create blobClient for container
  const blobClient = containerClient.getBlockBlobClient(file.name);

  // set mimetype as determined from browser with file upload control
  const options = { blobHTTPHeaders: { blobContentType: file.type } };

  // upload file
  await blobClient.uploadData(file, options);
}
// </snippet_createBlobInContainer>

// <snippet_uploadFileToBlob>
const uploadFileToBlob = async (file: File | null): Promise<string[]> => {
  if (!file) return [];

  // get BlobService = notice `?` is pulled out of sasToken - if created in Azure portal
  const blobService = new BlobServiceClient(
    `https://${storageAccountName}.blob.core.windows.net/?${sasToken}`
  );

  // get Container - full public read access
  const containerClient: ContainerClient = blobService.getContainerClient(containerName);
  await containerClient.createIfNotExists({
    access: 'container',
  });

  // upload file
  await createBlobInContainer(containerClient, file);

  // get list of blobs in container
  return getBlobsInContainer(containerClient);
};
// </snippet_uploadFileToBlob>

export default uploadFileToBlob;

Este código es el que viene del ejemplo que os he puesto anteriormente en el enlace.

Solo te queda crear en el fichero .env estas nuevas entradas, con los valores obtenidos del Azure Storage:

.Env de Teams

Ejecutas:

Ejecutado

Y listo ya tienes tu primera aplicación de Teams personalizada.

¿Por dónde continuar?

Mi sugerencia es que crees un ejemplo con backend, el ejemplo de TO-DO que te muestra como trabajar con Managed Identity y SSO, para que tengas una visión de como gestionar los token entre Teams, Frontend y Backend.

Y otra cosa que puedes investigar por tu cuenta ya son los Bots y los mensajes.

Hasta la proxima.

Si necesitas algun tipo de ayuda...

Al igual que escribo libros para ayudar a la comunidad, también puedo ayudarte en esto. Solo tienes que contactar via DM en LinkedIn o Twitter, los canales que más visito.