Publicar en Meta, instagram vamos

A continuación se presenta un ejemplo de código en C# que muestra la arquitectura y la implementación básica para la integración con la API de Meta (Instagram Graph API) dentro de un contexto DDD con AspNetBoilerplate. Este código es ilustrativo y no es un proyecto completo, sino que muestra las clases y métodos clave. En un entorno real, deberás:

  • Ajustar espacios de nombres (namespaces).
  • Integrar con la infraestructura real del proyecto (repositorios, entidades DDD, módulos de AspNetBoilerplate).
  • Configurar la obtención del AccessToken inicial (OAuth flow) fuera de este ejemplo.
  • Agregar manejo de errores, logging, pruebas unitarias y otras buenas prácticas.

Dependencias y Configuración Previa

.NET 5 o superior.

Paquetes NuGet:

  • Newtonsoft.Json para deserializar respuestas JSON.
  • Microsoft.Extensions.Http (opcional) para configurar HttpClient.

Se asume que ya se cuenta con un AccessToken de larga duración almacenado en algún repositorio seguro y métodos para refrescarlo.

Interfaces de Dominio

Estas interfaces definen las operaciones que el dominio de redes sociales requiere. Se encuentran en la capa de dominio o aplicación.

// Dominio/Aplicacion/Interfaces/IInstagramApiService.cs
public interface IInstagramApiService
{
    Task<InstagramInsightsDto> GetAccountMetricsAsync(string igUserId);
    Task<InstagramPostPublishResult> PublishImagePostAsync(string igUserId, string imageUrl, string caption);
    Task RefreshAccessTokenAsync(string accountId);
}

// DTOs (podrían ir en un namespace Application.Dto)
public class InstagramInsightsDto
{
    public int Impressions { get; set; }
    public int Reach { get; set; }
    public int ProfileViews { get; set; }
    // Agregar otras métricas según necesidad
}

public class InstagramPostPublishResult
{
    public string PostId { get; set; }
}

Entidades y Repositorio de Tokens

Se asume la existencia de una entidad SocialMediaAccount que almacena tokens. Puede ser parte del contexto de dominio "SocialMedia":

// Dominio/Entidades/SocialMediaAccount.cs
public class SocialMediaAccount
{
    public Guid Id { get; private set; }
    public string Platform { get; private set; } // "Instagram"
    public string InstagramUserId { get; private set; } // ID de usuario IG Business
    public string AccessToken { get; private set; }
    public DateTime AccessTokenExpiration { get; private set; }

    // Métodos para actualizar tokens
    public void UpdateAccessToken(string newToken, DateTime expiration)
    {
        AccessToken = newToken;
        AccessTokenExpiration = expiration;
    }

    // Otros atributos y métodos de dominio...
}

// Repositorio
public interface ISocialMediaAccountRepository
{
    Task<SocialMediaAccount> GetByInstagramUserIdAsync(string igUserId);
    Task<SocialMediaAccount> GetByIdAsync(Guid id);
    Task UpdateAsync(SocialMediaAccount account);
}

Implementación del Adaptador para la API de Instagram (Meta)

Este servicio vive en la capa de Infraestructura. Se conecta con la API de Instagram Graph y satisface la interfaz IInstagramApiService.

// Infraestructura/Servicios/InstagramApiAdapter.cs
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

public class InstagramApiAdapter : IInstagramApiService
{
    private readonly HttpClient _httpClient;
    private readonly ISocialMediaAccountRepository _accountRepository;
    private readonly string _facebookGraphVersion = "v15.0"; 
    // Asegúrate de usar la versión de la API que corresponda a tu caso.

    public InstagramApiAdapter(HttpClient httpClient, ISocialMediaAccountRepository accountRepository)
    {
        _httpClient = httpClient;
        _accountRepository = accountRepository;
    }

    public async Task<InstagramInsightsDto> GetAccountMetricsAsync(string igUserId)
    {
        var account = await _accountRepository.GetByInstagramUserIdAsync(igUserId);
        if (account == null)
        {
            throw new Exception("No se encontró la cuenta de Instagram asociada");
        }

        await EnsureAccessTokenIsValidAsync(account);

        var url = $"https://graph.facebook.com/{_facebookGraphVersion}/{igUserId}/insights" +
                  "?metric=impressions,reach,profile_views&period=day" +
                  $"&access_token={account.AccessToken}";

        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();

        // Estructura típica de la respuesta:
        // {
        //   "data": [
        //     { "name": "impressions", "values": [ { "value": 123 } ], ... },
        //     { "name": "reach", "values": [ { "value": 234 } ], ... },
        //     { "name": "profile_views", "values": [ { "value": 10 } ], ... }
        //   ],
        //   ...
        // }

        var insightsRoot = JsonConvert.DeserializeObject<InstagramInsightsRoot>(json);
        var result = new InstagramInsightsDto();

        if (insightsRoot?.Data != null)
        {
            foreach (var metric in insightsRoot.Data)
            {
                var value = metric.Values?[0]?.Value ?? 0;
                switch (metric.Name)
                {
                    case "impressions":
                        result.Impressions = value;
                        break;
                    case "reach":
                        result.Reach = value;
                        break;
                    case "profile_views":
                        result.ProfileViews = value;
                        break;
                }
            }
        }

        return result;
    }

    public async Task<InstagramPostPublishResult> PublishImagePostAsync(string igUserId, string imageUrl, string caption)
    {
        var account = await _accountRepository.GetByInstagramUserIdAsync(igUserId);
        if (account == null)
        {
            throw new Exception("No se encontró la cuenta de Instagram asociada");
        }

        await EnsureAccessTokenIsValidAsync(account);

        // Paso 1: Crear el contenedor de media
        var createMediaUrl = $"https://graph.facebook.com/{_facebookGraphVersion}/{igUserId}/media";
        var createMediaContent = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            {"image_url", imageUrl},
            {"caption", caption},
            {"access_token", account.AccessToken}
        });

        var mediaResponse = await _httpClient.PostAsync(createMediaUrl, createMediaContent);
        mediaResponse.EnsureSuccessStatusCode();
        var mediaJson = await mediaResponse.Content.ReadAsStringAsync();
        var mediaObj = JsonConvert.DeserializeObject<CreationMediaResponse>(mediaJson);
        var creationId = mediaObj.Id;

        // Paso 2: Publicar el post
        var publishUrl = $"https://graph.facebook.com/{_facebookGraphVersion}/{igUserId}/media_publish";
        var publishContent = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            {"creation_id", creationId},
            {"access_token", account.AccessToken}
        });

        var publishResponse = await _httpClient.PostAsync(publishUrl, publishContent);
        publishResponse.EnsureSuccessStatusCode();
        var publishJson = await publishResponse.Content.ReadAsStringAsync();
        var publishObj = JsonConvert.DeserializeObject<PublishResponse>(publishJson);

        return new InstagramPostPublishResult
        {
            PostId = publishObj.Id
        };
    }

    public async Task RefreshAccessTokenAsync(string accountId)
    {
        // Método para refrescar el token a largo plazo.
        // Deberás tener el long-lived token y el proceso para extenderlo.
        // Ver la documentación: https://developers.facebook.com/docs/instagram-api/getting-started

        var account = await _accountRepository.GetByIdAsync(Guid.Parse(accountId));
        if (account == null) throw new Exception("Cuenta no encontrada.");

        // Ejemplo de refreshing (ficticio, debe adaptarse a tu lógica):
        var refreshUrl = $"https://graph.facebook.com/{_facebookGraphVersion}/oauth/access_token?" +
                         $"grant_type=ig_refresh_token&access_token={account.AccessToken}";

        var response = await _httpClient.GetAsync(refreshUrl);
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        var tokenObj = JsonConvert.DeserializeObject<TokenRefreshResponse>(json);

        // Actualizar el token en la base de datos
        account.UpdateAccessToken(tokenObj.AccessToken, DateTime.UtcNow.AddDays(60)); // Ejemplo de vigencia
        await _accountRepository.UpdateAsync(account);
    }

    private async Task EnsureAccessTokenIsValidAsync(SocialMediaAccount account)
    {
        // Lógica para validar expiración y refrescar el token si es necesario
        if (account.AccessTokenExpiration <= DateTime.UtcNow.AddDays(1))
        {
            // El token expira pronto, refrescarlo
            // Esta lógica puede simplemente llamar a RefreshAccessTokenAsync 
            // si tenemos almacenado el accountId.
            
            await RefreshAccessTokenAsync(account.Id.ToString());
        }
    }
}

// Clases internas para deserializar las respuestas JSON
class InstagramInsightsRoot
{
    [JsonProperty("data")]
    public List<InsightsData> Data { get; set; }
}

class InsightsData
{
    [JsonProperty("name")]
    public string Name { get; set; }
    [JsonProperty("values")]
    public List<InsightsValue> Values { get; set; }
}

class InsightsValue
{
    [JsonProperty("value")]
    public int Value { get; set; }
}

class CreationMediaResponse
{
    [JsonProperty("id")]
    public string Id { get; set; }
}

class PublishResponse
{
    [JsonProperty("id")]
    public string Id { get; set; }
}

class TokenRefreshResponse
{
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }
    // Otros campos, ej: expires_in si aplica
}

Configuración del HttpClient

En el Startup.cs o el módulo de AspNetBoilerplate, se puede configurar el HttpClient y la inyección de dependencias:

// Startup.cs o equivalente
services.AddHttpClient<IInstagramApiService, InstagramApiAdapter>(client =>
{
    // Ajustes del HttpClient
});

Uso en un Servicio de Aplicación

Un ApplicationService (capa de aplicación AspNetBoilerplate) podría consumir IInstagramApiService así:

public class SocialMediaAppService : ApplicationService
{
    private readonly IInstagramApiService _instagramApiService;

    public SocialMediaAppService(IInstagramApiService instagramApiService)
    {
        _instagramApiService = instagramApiService;
    }

    public async Task<InstagramInsightsDto> GetAccountMetricsAsync(string igUserId)
    {
        return await _instagramApiService.GetAccountMetricsAsync(igUserId);
    }

    public async Task<string> PublishPostAsync(string igUserId, string imageUrl, string caption)
    {
        var result = await _instagramApiService.PublishImagePostAsync(igUserId, imageUrl, caption);
        return result.PostId;
    }
}

Conclusión

Este ejemplo presenta la estructura general del código para integrarse con la API de Meta (Instagram Graph API), incluyendo:

  • Interfaz de servicio de dominio/aplicación (IInstagramApiService).
  • Adaptador (InstagramApiAdapter) que encapsula las llamadas a la API externa.
  • Modelos de datos (DTOs) para deserializar respuestas.
  • Ejemplos de cómo actualizar el token y publicar posts.

En un escenario real, se requerirá:

  • Añadir manejo de errores más robusto.
  • Controlar excepciones por status codes no exitosos.
  • Implementar el flujo inicial de obtención de AccessToken.
  • Añadir pruebas unitarias y de integración.
  • Integrar con la lógica del dominio DDD existente y repositorios concretos (EF Core u otro).