Compare commits

..

No commits in common. "4cca23cb35c65440ec5a2e10281f82d3646afba1" and "4b04639ad76330f538d67859f90c575d2e15a945" have entirely different histories.

52 changed files with 95 additions and 2988 deletions

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>

View File

@ -6,16 +6,16 @@ using System.Threading.Tasks;
namespace BaseDomain.Results
{
public enum ErrorTypeEnum
{
None = 0, //Erro vazio (para result)
Failure = 1, //Quando for uma falha que eu queira retornar no lugar de uma Exception
Validation = 2, //Quando for um problema de validação
Others = 3 //Inesperado ou sem categoria
}
public class Error
{
public enum ErrorTypeEnum
{
None = 0, //Erro vazio (para result)
Failure = 1, //Quando for uma falha que eu queira retornar no lugar de uma Exception
Validation = 2, //Quando for um problema de validação
Others = 3 //Inesperado ou sem categoria
}
public Error(ErrorTypeEnum error, string message, string description="")
{
ErrorType = error;

View File

@ -10,7 +10,7 @@ namespace BaseDomain.Results
{
protected Result(bool isSuccess, Error error)
{
if (isSuccess && error.ErrorType != ErrorTypeEnum.None)
if (isSuccess && error != Error.None)
{
throw new InvalidOperationException();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -11,8 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Postall.Domain", "Postall.D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BaseDomain", "BaseDomain\BaseDomain.csproj", "{72324CE0-8009-4876-9CF9-9F2BA8288B87}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Postall.Infra.MongoDB", "Postall.Infra.MongoDB\Postall.Infra.MongoDB.csproj", "{2EFC97F7-84D3-4582-9202-B03B401D9E9F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -35,10 +33,6 @@ Global
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Release|Any CPU.Build.0 = Release|Any CPU
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,26 +0,0 @@
using BaseDomain.Results;
using Postall.Domain.Dtos;
using Postall.Domain.Services.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain.Application
{
public class ChannelApplicationService
{
private readonly IChannelService _channelService;
public ChannelApplicationService(IChannelService channelService)
{
_channelService = channelService;
}
public async Task<Result<List<ChannelResponse>>> GetUserChannelsAsync()
{
return await _channelService.GetUserChannelsAsync();
}
}
}

View File

@ -1,44 +0,0 @@
using Postall.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain.Dtos
{
public class ChannelResponse
{
public string Id { get; set; }
public string UserId { get; set; }
public string YoutubeId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string ThumbnailUrl { get; set; }
public DateTime PublishedAt { get; set; }
public ulong SubscriberCount { get; set; }
public ulong VideoCount { get; set; }
// Propriedade para seleção de canais na interface
public bool IsSelected { get; set; }
// URL do canal no YouTube
public string ChannelUrl => $"https://www.youtube.com/channel/{Id}";
public ChannelData ToChannelData()
{
return new ChannelData
{
Id = Id,
UserId = UserId,
YoutubeId = YoutubeId,
Title = Title,
Description = Description,
ThumbnailUrl = ThumbnailUrl,
PublishedAt = PublishedAt,
SubscriberCount = SubscriberCount,
VideoCount = VideoCount
};
}
}
}

View File

@ -1,17 +0,0 @@
// Models/FacebookTokenResponse.cs
using System.Text.Json.Serialization;
namespace Postall.Domain.Dtos.Responses
{
public class FacebookTokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("expires_in")]
public long ExpiresIn { get; set; }
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Infra.Dtos
{
public class VideoItemResponse
{
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string ThumbnailUrl { get; set; }
public DateTime PublishedAt { get; set; }
public bool IsSelected { get; set; }
// Propriedades adicionais que podem ser úteis no futuro
public string ChannelTitle { get; set; }
public string ChannelId { get; set; }
public string VideoUrl => $"https://www.youtube.com/watch?v={Id}";
}
}

View File

@ -1,67 +0,0 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Postall.Domain.Dtos;
namespace Postall.Domain.Entities
{
/// <summary>
/// Modelo de dados para armazenamento do canal no MongoDB
/// </summary>
public class ChannelData
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
[BsonElement("userId")]
public string UserId { get; set; }
[BsonElement("channelId")]
public string ChannelId { get; set; }
[BsonElement("youtubeId")]
public string YoutubeId { get; set; }
[BsonElement("title")]
public string Title { get; set; }
[BsonElement("description")]
public string Description { get; set; }
[BsonElement("thumbnailUrl")]
public string ThumbnailUrl { get; set; }
[BsonElement("publishedAt")]
public DateTime PublishedAt { get; set; }
[BsonElement("subscriberCount")]
public ulong SubscriberCount { get; set; }
[BsonElement("videoCount")]
public ulong VideoCount { get; set; }
[BsonElement("isSelected")]
public bool IsSelected { get; set; }
[BsonIgnore]
public string ChannelUrl => $"https://www.youtube.com/channel/{YoutubeId}";
public ChannelResponse ToChannelResponse() => new ChannelResponse
{
Id = Id,
UserId = UserId,
YoutubeId = YoutubeId,
Title = Title,
Description = Description,
ThumbnailUrl = ThumbnailUrl,
PublishedAt = PublishedAt,
SubscriberCount = SubscriberCount,
VideoCount = VideoCount
};
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain.Entities
{
public class FacebookToken
{
public string AccessToken { get; set; }
public DateTime ExpiresAt { get; set; }
}
}

View File

@ -1,20 +0,0 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Postall.Domain.Entities
{
public class UserSocialData
{
[BsonId]
public ObjectId Id { get; set; }
public string UserId { get; set; }
public string GoogleToken { get; set; }
public FacebookToken FacebookToken { get; set; }
}
}

View File

@ -1,92 +0,0 @@
using Postall.Domain.Dtos;
using Postall.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain
{
/// <summary>
/// Interface para repositório de canais do YouTube no MongoDB
/// </summary>
public interface IChannelRepository
{
/// <summary>
/// Obtém todos os canais
/// </summary>
Task<IEnumerable<ChannelData>> GetAllAsync();
/// <summary>
/// Obtém um canal pelo ID do MongoDB
/// </summary>
Task<ChannelData> GetByIdAsync(string id);
/// <summary>
/// Obtém os canais pelo ID do usuario do MongoDB
/// </summary>
Task<IList<ChannelData>> GetByUserIdAsync(string userId);
/// <summary>
/// Obtém os canais pelo ID do usuario e o channelId
/// </summary>
Task<ChannelData> GetByUserIdAndChannelIdAsync(string userId, string channelId);
/// <summary>
/// Obtém um canal pelo ID do YouTube
/// </summary>
Task<ChannelData> GetByYoutubeIdAsync(string youtubeId);
/// <summary>
/// Adiciona um novo canal
/// </summary>
Task<ChannelData> AddAsync(ChannelData ChannelData);
/// <summary>
/// Adiciona vários canais de uma vez
/// </summary>
Task<IEnumerable<ChannelData>> AddManyAsync(IEnumerable<ChannelData> channels);
/// <summary>
/// Atualiza um canal existente
/// </summary>
Task<bool> UpdateAsync(ChannelData ChannelData);
/// <summary>
/// Remove um canal pelo ID do MongoDB
/// </summary>
Task<bool> DeleteAsync(string id);
/// <summary>
/// Remove um canal pelo ID do YouTube
/// </summary>
Task<bool> DeleteByYoutubeIdAsync(string youtubeId);
/// <summary>
/// Busca canais com base em um termo de pesquisa no título ou descrição
/// </summary>
Task<IEnumerable<ChannelData>> SearchAsync(string searchTerm);
/// <summary>
/// Obtém canais selecionados (IsSelected = true)
/// </summary>
Task<IEnumerable<ChannelData>> GetSelectedAsync();
/// <summary>
/// Marca ou desmarca um canal como selecionado
/// </summary>
Task<bool> SetSelectedStatusAsync(string id, bool isSelected);
/// <summary>
/// Converte ChannelResponse para ChannelData
/// </summary>
ChannelData ConvertFromResponse(ChannelResponse channelResponse);
/// <summary>
/// Converte ChannelData para ChannelResponse
/// </summary>
ChannelResponse ConvertToResponse(ChannelData ChannelData);
}
}

View File

@ -7,9 +7,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="libc.eventbus" Version="7.1.2" />
<PackageReference Include="MongoDB.Bson" Version="3.1.0" />
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
@ -18,11 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BaseDomain\BaseDomain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Events\" />
<ProjectReference Include="..\..\vcart.me\vcart.back\Struct.ValueObjects\BaseDomain.csproj" />
</ItemGroup>
</Project>

View File

@ -1,171 +0,0 @@
using BaseDomain.Results;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Postall.Domain;
using Postall.Domain.Dtos;
using Postall.Domain.Entities;
using Postall.Domain.Services.Contracts;
using System.Security.Claims;
namespace Postall.Infra.Services
{
public class ChannelVideoService: IChannelService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IChannelRepository _channelRepository;
private readonly IChannelYoutubeService _channelYoutubeService;
private readonly IConfiguration _configuration;
private readonly string _apiKey;
public ChannelVideoService(
IHttpContextAccessor httpContextAccessor,
IChannelRepository channelRepository,
IChannelYoutubeService channelYoutubeService
)
{
_httpContextAccessor = httpContextAccessor;
this._channelRepository = channelRepository;
this._channelYoutubeService = channelYoutubeService;
}
/// <summary>
/// Obtém o ID do usuário atual a partir das claims
/// </summary>
private string GetCurrentUserId()
{
var user = _httpContextAccessor.HttpContext.User;
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
var email = user.FindFirst(ClaimTypes.Email)?.Value;
if (!string.IsNullOrEmpty(email))
{
userId = Convert.ToBase64String(
System.Text.Encoding.UTF8.GetBytes(email)
).Replace("/", "_").Replace("+", "-").Replace("=", "");
}
}
return userId;
}
/// <summary>
/// Obtém a lista de canais que o usuário tem permissão para listar
/// </summary>
public async Task<Result<List<ChannelResponse>>> GetUserChannelsAsync()
{
var userId = GetCurrentUserId();
if (string.IsNullOrEmpty(userId))
return new List<ChannelResponse>();
var channelList = await _channelRepository.GetByUserIdAsync(userId);
if (!channelList.Any())
{
var defaultChannels = new List<ChannelData>
{
new ChannelData { Id = "UC_x5XG1OV2P6uZZ5FSM9Ttw", Title = "Google Developers" },
new ChannelData { Id = "UCkw4JCwteGrDHIsyIIKo4tQ", Title = "Google" }
};
foreach (var channel in defaultChannels)
{
var channelDetails = await this.GetChannelDetailsAsync(channel.Id);
if (channelDetails.IsSuccess)
{
channelList.Add(channelDetails.Value.ToChannelData());
}
else
{
Console.WriteLine($"Erro ao buscar detalhes do canal {channel.Id}: {channelDetails.Error.Description}");
channelList.Add(new ChannelData
{
Id = channel.Id,
Title = channel.Title,
ThumbnailUrl = "/images/default-channel-thumb.jpg" // Imagem padrão
});
}
}
}
return channelList.Select(c => c.ToChannelResponse()).ToList();
}
/// <summary>
/// Adiciona um canal à lista do usuário
/// </summary>
public async Task<Result<bool>> AddChannelAsync(string channelId)
{
if (string.IsNullOrEmpty(channelId))
return false;
var userId = GetCurrentUserId();
if (string.IsNullOrEmpty(userId))
return false;
var channelItem = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
try
{
if (channelItem != null)
return false;
var channelDetails = await GetChannelDetailsAsync(channelId);
await _channelRepository.AddAsync(channelDetails.Value.ToChannelData());
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Erro ao adicionar canal {channelId}: {ex.Message}");
return false;
}
}
/// <summary>
/// Remove um canal da lista do usuário
/// </summary>
public async Task<Result<bool>> RemoveChannel(string channelId)
{
if (string.IsNullOrEmpty(channelId))
return false;
var userId = GetCurrentUserId();
var channelItem = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
if (channelItem == null)
return false;
await _channelRepository.DeleteAsync(channelId);
return true;
}
/// <summary>
/// Busca os detalhes de um canal na API do YouTube
/// </summary>
public async Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId)
{
var channel = await _channelYoutubeService.GetChannelDetailsAsync(channelId);
return channel;
}
/// <summary>
/// Busca um canal pelo nome ou ID
/// </summary>
public async Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5)
{
if (string.IsNullOrEmpty(query) || query.Length < 3)
return new List<ChannelResponse>();
var results = await _channelYoutubeService.SearchChannelsAsync(query, maxResults);
return results;
}
}
}

View File

@ -1,23 +0,0 @@
using BaseDomain.Results;
using Postall.Domain.Dtos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain.Services.Contracts
{
public interface IChannelService
{
Task<Result<List<ChannelResponse>>> GetUserChannelsAsync();
Task<Result<bool>> AddChannelAsync(string channelId);
Task<Result<bool>> RemoveChannel(string channelId);
Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5);
Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId);
}
}

View File

@ -1,17 +0,0 @@
using BaseDomain.Results;
using Postall.Domain.Dtos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain.Services.Contracts
{
public interface IChannelYoutubeService
{
Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5);
Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId);
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain.Services.Contracts
{
public interface IFacebookServices
{
Task<string> GetLongLivedToken(string shortLivedToken);
Task SaveFacebookToken(string userId, string token);
}
}

View File

@ -1,20 +0,0 @@
using Postall.Infra.Dtos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Domain.Services.Contracts
{
public interface IVideoService
{
Task<List<VideoItemResponse>> GetUserChannelVideosAsync(int maxResults = 10);
Task<List<VideoItemResponse>> GetChannelVideosAsync(string channelId, int maxResults = 10);
Task SaveVideoForUserAsync(string videoId);
Task RemoveVideoForUserAsync(string videoId);
}
}

View File

@ -1,23 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Postall.Domain;
using Postall.Domain.Services.Contracts;
using Postall.Infra.MongoDB.Repositories;
using Postall.Infra.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Infra.MongoDB.Extensions
{
public static class ServiceRepositoryExtensions
{
public static IServiceCollection AddRepositories(this IServiceCollection services)
{
services.AddScoped<IChannelRepository, ChannelRepository>();
return services;
}
}
}

View File

@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-preview.1.25080.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-preview.1.25080.5" />
<PackageReference Include="MongoDB.Driver" Version="3.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -1,244 +0,0 @@
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Postall.Domain;
using Postall.Domain.Dtos;
using Postall.Domain.Entities;
using Postall.Infra.MongoDB.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Channels;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Postall.Infra.MongoDB.Repositories
{
public class ChannelRepository : IChannelRepository
{
private readonly IMongoCollection<ChannelData> _channelsCollection;
public ChannelRepository(IOptions<MongoDbSettings> mongoDbSettings)
{
var client = new MongoClient(mongoDbSettings.Value.ConnectionString);
var database = client.GetDatabase(mongoDbSettings.Value.DatabaseName);
_channelsCollection = database.GetCollection<ChannelData>(mongoDbSettings.Value.ChannelsCollectionName);
var indexKeysDefinition = Builders<ChannelData>.IndexKeys.Ascending(c => c.YoutubeId);
_channelsCollection.Indexes.CreateOne(new CreateIndexModel<ChannelData>(indexKeysDefinition, new CreateIndexOptions { Unique = true }));
var textIndexDefinition = Builders<ChannelData>.IndexKeys
.Text(c => c.Title)
.Text(c => c.Description);
_channelsCollection.Indexes.CreateOne(new CreateIndexModel<ChannelData>(textIndexDefinition));
}
/// <summary>
/// Obtém todos os canais
/// </summary>
public async Task<IEnumerable<ChannelData>> GetAllAsync()
{
return await _channelsCollection.Find(c => true).ToListAsync();
}
/// <summary>
/// Obtém um canal pelo ID do MongoDB
/// </summary>
public async Task<ChannelData> GetByIdAsync(string id)
{
if (!ObjectId.TryParse(id, out _))
return null;
return await _channelsCollection.Find(c => c.Id == id).FirstOrDefaultAsync();
}
public async Task<IList<ChannelData>> GetByUserIdAsync(string userId)
{
return await _channelsCollection.Find(c => c.UserId == userId).ToListAsync();
}
/// <summary>
/// Obtém os canais pelo ID do usuario e o channelId
/// </summary>
public async Task<ChannelData> GetByUserIdAndChannelIdAsync(string userId, string channelId)
{
return await _channelsCollection.Find(c => c.UserId == userId && c.ChannelId == channelId).FirstOrDefaultAsync();
}
/// <summary>
/// Obtém um canal pelo ID do YouTube
/// </summary>
public async Task<ChannelData> GetByYoutubeIdAsync(string youtubeId)
{
return await _channelsCollection.Find(c => c.YoutubeId == youtubeId).FirstOrDefaultAsync();
}
/// <summary>
/// Adiciona um novo canal
/// </summary>
public async Task<ChannelData> AddAsync(ChannelData ChannelData)
{
// Verifica se o canal já existe pelo ID do YouTube
var existingChannel = await GetByYoutubeIdAsync(ChannelData.YoutubeId);
if (existingChannel != null)
return null;
// Gera novo ID para o MongoDB se não for fornecido
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
{
ChannelData.Id = ObjectId.GenerateNewId().ToString();
}
await _channelsCollection.InsertOneAsync(ChannelData);
return ChannelData;
}
/// <summary>
/// Adiciona vários canais de uma vez
/// </summary>
public async Task<IEnumerable<ChannelData>> AddManyAsync(IEnumerable<ChannelData> channels)
{
if (channels == null || !channels.Any())
return new List<ChannelData>();
var channelsList = channels.ToList();
// Gera novos IDs para o MongoDB se necessário
foreach (var ChannelData in channelsList)
{
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
{
ChannelData.Id = ObjectId.GenerateNewId().ToString();
}
}
// Verifica canais existentes pelo ID do YouTube
var youtubeIds = channelsList.Select(c => c.YoutubeId).ToList();
var existingChannels = await _channelsCollection
.Find(c => youtubeIds.Contains(c.YoutubeId))
.ToListAsync();
var existingYoutubeIds = existingChannels.Select(c => c.YoutubeId).ToHashSet();
var newChannels = channelsList.Where(c => !existingYoutubeIds.Contains(c.YoutubeId)).ToList();
if (newChannels.Any())
await _channelsCollection.InsertManyAsync(newChannels);
return newChannels;
}
/// <summary>
/// Atualiza um canal existente
/// </summary>
public async Task<bool> UpdateAsync(ChannelData ChannelData)
{
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
return false;
var result = await _channelsCollection.ReplaceOneAsync(
c => c.Id == ChannelData.Id,
ChannelData,
new ReplaceOptions { IsUpsert = false });
return result.IsAcknowledged && result.ModifiedCount > 0;
}
/// <summary>
/// Remove um canal pelo ID do MongoDB
/// </summary>
public async Task<bool> DeleteAsync(string id)
{
if (!ObjectId.TryParse(id, out _))
return false;
var result = await _channelsCollection.DeleteOneAsync(c => c.Id == id);
return result.IsAcknowledged && result.DeletedCount > 0;
}
/// <summary>
/// Remove um canal pelo ID do YouTube
/// </summary>
public async Task<bool> DeleteByYoutubeIdAsync(string youtubeId)
{
var result = await _channelsCollection.DeleteOneAsync(c => c.YoutubeId == youtubeId);
return result.IsAcknowledged && result.DeletedCount > 0;
}
/// <summary>
/// Busca canais com base em um termo de pesquisa no título ou descrição
/// </summary>
public async Task<IEnumerable<ChannelData>> SearchAsync(string searchTerm)
{
if (string.IsNullOrWhiteSpace(searchTerm))
return await GetAllAsync();
var filter = Builders<ChannelData>.Filter.Text(searchTerm);
return await _channelsCollection.Find(filter).ToListAsync();
}
/// <summary>
/// Obtém canais selecionados (IsSelected = true)
/// </summary>
public async Task<IEnumerable<ChannelData>> GetSelectedAsync()
{
return await _channelsCollection.Find(c => c.IsSelected).ToListAsync();
}
/// <summary>
/// Marca ou desmarca um canal como selecionado
/// </summary>
public async Task<bool> SetSelectedStatusAsync(string id, bool isSelected)
{
if (!ObjectId.TryParse(id, out _))
return false;
var update = Builders<ChannelData>.Update.Set(c => c.IsSelected, isSelected);
var result = await _channelsCollection.UpdateOneAsync(c => c.Id == id, update);
return result.IsAcknowledged && result.ModifiedCount > 0;
}
/// <summary>
/// Converte ChannelResponse para ChannelData
/// </summary>
public ChannelData ConvertFromResponse(ChannelResponse channelResponse)
{
if (channelResponse == null)
return null;
return new ChannelData
{
YoutubeId = channelResponse.Id,
Title = channelResponse.Title,
Description = channelResponse.Description,
ThumbnailUrl = channelResponse.ThumbnailUrl,
PublishedAt = channelResponse.PublishedAt,
SubscriberCount = channelResponse.SubscriberCount,
VideoCount = channelResponse.VideoCount,
IsSelected = channelResponse.IsSelected
};
}
/// <summary>
/// Converte ChannelData para ChannelResponse
/// </summary>
public ChannelResponse ConvertToResponse(ChannelData ChannelData)
{
if (ChannelData == null)
return null;
return new ChannelResponse
{
Id = ChannelData.YoutubeId,
Title = ChannelData.Title,
Description = ChannelData.Description,
ThumbnailUrl = ChannelData.ThumbnailUrl,
PublishedAt = ChannelData.PublishedAt,
SubscriberCount = ChannelData.SubscriberCount,
VideoCount = ChannelData.VideoCount,
IsSelected = ChannelData.IsSelected
};
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Infra.MongoDB.Settings
{
/// <summary>
/// Configuração do MongoDB
/// </summary>
public class MongoDbSettings
{
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
public string ChannelsCollectionName { get; set; }
}
}

View File

@ -1,23 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Postall.Domain;
using Postall.Domain.Services.Contracts;
using Postall.Infra.Services;
namespace BaseDomain.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddYouTubeServices(this IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddScoped<IVideoService, YouTubeServiceVideo>();
services.AddScoped<IChannelService, ChannelVideoService>();
services.AddScoped<IChannelYoutubeService, ChannelYoutubeService>();
return services;
}
}
}

View File

@ -7,9 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.69.0.3707" />
<PackageReference Include="MongoDB.Bson" Version="3.1.0" />
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
<PackageReference Include="MongoDB.Bson" Version="2.28.0" />
<PackageReference Include="MongoDB.Driver" Version="2.28.0" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
@ -17,8 +16,4 @@
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -1,96 +0,0 @@
using BaseDomain.Results;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Postall.Domain.Dtos;
using Postall.Domain.Services.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Infra.Services
{
public class ChannelYoutubeService: IChannelYoutubeService
{
private readonly IConfiguration _configuration;
private readonly string _apiKey;
public ChannelYoutubeService(IConfiguration configuration)
{
_configuration = configuration;
_apiKey = _configuration["YouTube:ApiKey"];
}
/// <summary>
/// Busca os detalhes de um canal na API do YouTube
/// </summary>
public async Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId)
{
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
var channelsRequest = youtubeService.Channels.List("snippet,statistics,contentDetails");
channelsRequest.Id = channelId;
var response = await channelsRequest.ExecuteAsync();
if (response.Items.Count == 0)
throw new Exception($"Canal não encontrado: {channelId}");
var channel = response.Items[0];
return new ChannelResponse
{
Id = channel.Id,
Title = channel.Snippet.Title,
Description = channel.Snippet.Description,
ThumbnailUrl = channel.Snippet.Thumbnails.Default__.Url,
PublishedAt = channel.Snippet.PublishedAt.Value,
SubscriberCount = channel.Statistics.SubscriberCount.GetValueOrDefault(),
VideoCount = channel.Statistics.VideoCount.GetValueOrDefault()
};
}
/// <summary>
/// Busca um canal pelo nome ou ID
/// </summary>
public async Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5)
{
if (string.IsNullOrEmpty(query) || query.Length < 3)
return new List<ChannelResponse>();
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
var searchRequest = youtubeService.Search.List("snippet");
searchRequest.Q = query;
searchRequest.Type = "channel";
searchRequest.MaxResults = maxResults;
var searchResponse = await searchRequest.ExecuteAsync();
var results = new List<ChannelResponse>();
foreach (var item in searchResponse.Items)
{
results.Add(new ChannelResponse
{
Id = item.Id.ChannelId,
Title = item.Snippet.Title,
Description = item.Snippet.Description,
ThumbnailUrl = item.Snippet.Thumbnails.Default__.Url,
PublishedAt = item.Snippet.PublishedAt.Value
});
}
return results;
}
}
// Classe auxiliar para dados básicos de canal
public class ChannelData
{
public string Id { get; set; }
public string Title { get; set; }
}
}

View File

@ -1,49 +0,0 @@
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
using Postall.Domain.Dtos.Responses;
using Postall.Domain.Entities;
using Postall.Domain.Services.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;
namespace Postall.Infra.Services
{
public class FacebookTokenService: IFacebookServices
{
private readonly IMongoCollection<UserSocialData> _tokens;
private readonly IConfiguration _config;
private readonly HttpClient _httpClient;
public async Task<string> GetLongLivedToken(string shortLivedToken)
{
var appId = _config["Authentication:Facebook:AppId"];
var appSecret = _config["Authentication:Facebook:AppSecret"];
var response = await _httpClient.GetFromJsonAsync<FacebookTokenResponse>(
$"https://graph.facebook.com/oauth/access_token?" +
$"grant_type=fb_exchange_token&" +
$"client_id={appId}&" +
$"client_secret={appSecret}&" +
$"fb_exchange_token={shortLivedToken}");
return response.AccessToken;
}
public async Task SaveFacebookToken(string userId, string token)
{
var update = Builders<UserSocialData>.Update
.Set(x => x.FacebookToken.AccessToken, token)
.Set(x => x.FacebookToken.ExpiresAt, DateTime.UtcNow.AddDays(60));
await _tokens.UpdateOneAsync(
x => x.UserId == userId,
update,
new UpdateOptions { IsUpsert = true }
);
}
}
}

View File

@ -1,227 +0,0 @@
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Postall.Domain.Services.Contracts;
using Postall.Infra.Dtos;
using System.Security.Claims;
namespace Postall.Infra.Services
{
public class YouTubeServiceVideo: IVideoService
{
private readonly IConfiguration _configuration;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly string _apiKey;
private readonly string _clientId;
private readonly string _clientSecret;
public YouTubeServiceVideo(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
_apiKey = _configuration["YouTube:ApiKey"];
_clientId = _configuration["Authentication:Google:ClientId"];
_clientSecret = _configuration["Authentication:Google:ClientSecret"];
}
/// <summary>
/// Obtém o ID do usuário atual a partir das claims
/// </summary>
private string GetCurrentUserId()
{
var user = _httpContextAccessor.HttpContext.User;
// Primeiro tentar obter o NameIdentifier (que foi adicionado no ExternalLoginCallback)
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Se não encontrou, tenta usar o email como identificador
if (string.IsNullOrEmpty(userId))
{
var email = user.FindFirst(ClaimTypes.Email)?.Value;
if (!string.IsNullOrEmpty(email))
{
// Gerar ID baseado no email (mesmo método usado no ExternalLoginCallback)
userId = Convert.ToBase64String(
System.Text.Encoding.UTF8.GetBytes(email)
).Replace("/", "_").Replace("+", "-").Replace("=", "");
}
}
return userId;
}
/// <summary>
/// Obtém os vídeos dos canais que o usuário tem acesso, ordenados por data
/// </summary>
public async Task<List<VideoItemResponse>> GetUserChannelVideosAsync(int maxResults = 10)
{
// Obter o ID do usuário das claims
var userId = GetCurrentUserId();
if (string.IsNullOrEmpty(userId))
return new List<VideoItemResponse>();
// Obter os canais do usuário do banco de dados
var userChannels = await GetUserChannelsAsync(userId);
if (!userChannels.Any())
return new List<VideoItemResponse>();
var videos = new List<VideoItemResponse>();
// Para cada canal, buscar os vídeos recentes
foreach (var channelId in userChannels)
{
var channelVideos = await GetChannelVideosAsync(channelId, maxResults);
videos.AddRange(channelVideos);
}
// Ordenar por data de publicação e pegar os mais recentes
return videos
.OrderByDescending(v => v.PublishedAt)
.Take(maxResults)
.ToList();
}
/// <summary>
/// Obtém os vídeos de um canal específico
/// </summary>
public async Task<List<VideoItemResponse>> GetChannelVideosAsync(string channelId, int maxResults = 10)
{
try
{
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
// Primeiro, buscar os IDs dos últimos vídeos do canal
var searchRequest = youtubeService.Search.List("snippet");
searchRequest.ChannelId = channelId;
searchRequest.Order = SearchResource.ListRequest.OrderEnum.Date;
searchRequest.MaxResults = maxResults;
searchRequest.Type = "video";
var searchResponse = await searchRequest.ExecuteAsync();
if (searchResponse.Items == null || searchResponse.Items.Count == 0)
return new List<VideoItemResponse>();
// Obter os IDs dos vídeos
var videoIds = searchResponse.Items.Select(i => i.Id.VideoId).ToList();
// Buscar detalhes dos vídeos
var videosRequest = youtubeService.Videos.List("snippet,contentDetails,statistics");
videosRequest.Id = string.Join(",", videoIds);
var videosResponse = await videosRequest.ExecuteAsync();
// Converter para o modelo
var videos = new List<VideoItemResponse>();
foreach (var videoItem in videosResponse.Items)
{
var searchItem = searchResponse.Items.FirstOrDefault(i => i.Id.VideoId == videoItem.Id);
videos.Add(new VideoItemResponse
{
Id = videoItem.Id,
Title = videoItem.Snippet.Title,
Description = videoItem.Snippet.Description,
ThumbnailUrl = videoItem.Snippet.Thumbnails.High.Url,
PublishedAt = videoItem.Snippet.PublishedAt.Value,
ChannelId = videoItem.Snippet.ChannelId,
ChannelTitle = videoItem.Snippet.ChannelTitle
});
}
return videos;
}
catch (Exception ex)
{
// Em produção, use um logger adequado
Console.WriteLine($"Erro ao obter vídeos do canal {channelId}: {ex.Message}");
return new List<VideoItemResponse>();
}
}
/// <summary>
/// Método para buscar os canais que o usuário gerencia ou tem acesso
/// Em um cenário real, isso viria do banco de dados
/// </summary>
private async Task<List<string>> GetUserChannelsAsync(string userId)
{
// TODO: Substituir isso por uma consulta real ao banco de dados
// Em produção, você deve implementar uma tabela que relacione
// os usuários com seus canais do YouTube
// Para teste, aqui estão alguns canais populares
// Na implementação real, você deve armazenar os canais que o usuário tem permissão
return new List<string>
{
"UC_x5XG1OV2P6uZZ5FSM9Ttw", // Google Developers
"UCt84aUC9OG6di8kSdKzEHTQ", // Outro canal de exemplo
"UCkw4JCwteGrDHIsyIIKo4tQ" // Google
};
}
/// <summary>
/// Salva um vídeo para o usuário atual
/// Em produção, isso salvaria no banco de dados
/// </summary>
public async Task SaveVideoForUserAsync(string videoId)
{
var userId = GetCurrentUserId();
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(videoId))
return;
// TODO: Salvar no banco de dados a relação entre o usuário e o vídeo
// Código de exemplo:
// await _dbContext.UserVideos.AddAsync(new UserVideo
// {
// UserId = userId,
// VideoId = videoId,
// DateAdded = DateTime.UtcNow
// });
// await _dbContext.SaveChangesAsync();
}
/// <summary>
/// Remove um vídeo do usuário atual
/// Em produção, isso removeria do banco de dados
/// </summary>
public async Task RemoveVideoForUserAsync(string videoId)
{
var userId = GetCurrentUserId();
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(videoId))
return;
// TODO: Remover do banco de dados a relação entre o usuário e o vídeo
// Código de exemplo:
// var userVideo = await _dbContext.UserVideos
// .FirstOrDefaultAsync(uv => uv.UserId == userId && uv.VideoId == videoId);
// if (userVideo != null)
// {
// _dbContext.UserVideos.Remove(userVideo);
// await _dbContext.SaveChangesAsync();
// }
}
}
/// <summary>
/// Wrapper para o serviço base do YouTube
/// </summary>
public class YouTubeBaseServiceWrapper : YouTubeService
{
public YouTubeBaseServiceWrapper(string apiKey)
: base(new BaseClientService.Initializer()
{
ApiKey = apiKey,
ApplicationName = "PostAll"
})
{
}
}
}

View File

@ -1,98 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Postall.Domain.Services.Contracts;
using Postall.Infra.Services;
namespace Postall.Controllers
{
[Authorize]
public class ChannelsController : Controller
{
private readonly IChannelService _channelService;
public ChannelsController(IChannelService channelService)
{
_channelService = channelService;
}
public async Task<IActionResult> Index()
{
var userChannels = await _channelService.GetUserChannelsAsync();
return View(userChannels);
}
[HttpGet]
public IActionResult Add()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Add(string channelId)
{
if (string.IsNullOrEmpty(channelId))
{
TempData["Error"] = "ID do canal é obrigatório";
return RedirectToAction("Add");
}
try
{
var success = await _channelService.AddChannelAsync(channelId);
if (success.IsSuccess)
{
TempData["Message"] = "Canal adicionado com sucesso!";
return RedirectToAction("Index");
}
TempData["Error"] = "Não foi possível adicionar o canal. Ele já existe ou é inválido.";
return RedirectToAction("Add");
}
catch (Exception ex)
{
TempData["Error"] = $"Erro ao adicionar canal: {ex.Message}";
return RedirectToAction("Add");
}
}
[HttpPost]
public async Task<IActionResult> Remove(string id)
{
try
{
var success = await _channelService.RemoveChannel(id);
if (success.IsSuccess)
TempData["Message"] = "Canal removido com sucesso!";
else
TempData["Error"] = "Não foi possível remover o canal.";
}
catch (Exception ex)
{
TempData["Error"] = $"Erro ao remover canal: {ex.Message}";
}
return RedirectToAction("Index");
}
[HttpGet]
public async Task<IActionResult> Search(string query)
{
if (string.IsNullOrEmpty(query) || query.Length < 3)
{
return Json(new { success = false, message = "A consulta deve ter pelo menos 3 caracteres" });
}
try
{
var results = await _channelService.SearchChannelsAsync(query);
return Json(new { success = true, data = results });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
}
}

View File

@ -44,70 +44,63 @@ namespace Postall.Controllers
return Challenge(properties, "Google");
}
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult> ExternalLoginCallback(string code = "")
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult> ExternalLoginCallback(string code="")
{
var emailExist = HttpContext.User.FindFirst(ClaimTypes.Email).Value;
if (emailExist != null)
{
var uniqueId = Convert.ToBase64String(
System.Text.Encoding.UTF8.GetBytes(emailExist)
).Replace("/", "_").Replace("+", "-").Replace("=", "");
var claims = new List<Claim>
{
// Adicionando o NameIdentifier que serve como userId
new Claim(ClaimTypes.NameIdentifier, uniqueId),
// Claims existentes
new Claim(ClaimTypes.Name, emailExist),
new Claim(ClaimTypes.Email, emailExist), // Garantindo que o email esteja nas claims
//TODO: Temporário
var emailExist = HttpContext.User.FindFirst(ClaimTypes.Email).Value;
if (emailExist != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, emailExist),
new Claim("FirstName", HttpContext.User.FindFirst(ClaimTypes.GivenName).Value),
new Claim("FullName", HttpContext.User.FindFirst(ClaimTypes.GivenName).Value + " " + HttpContext.User.FindFirst(ClaimTypes.Surname).Value),
new Claim(ClaimTypes.Role, "User"),
new Claim(ClaimTypes.Role, "User"),
};
// Opcionalmente, adicionar informações do Google que serão úteis para o YouTube
new Claim("GoogleAccount", "true")
};
var claimsIdentity = new ClaimsIdentity(
claims,
CookieAuthenticationDefaults.AuthenticationScheme
);
var claimsIdentity = new ClaimsIdentity(
claims,
CookieAuthenticationDefaults.AuthenticationScheme
);
var authProperties = new AuthenticationProperties
{
//AllowRefresh = <bool>,
// Refreshing the authentication session should be allowed.
var authProperties = new AuthenticationProperties
{
//AllowRefresh = <bool>,
// Refreshing the authentication session should be allowed.
//ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.
//IsPersistent = true,
// Whether the authentication session is persisted across
// multiple requests. When used with cookies, controls
// whether the cookie's lifetime is absolute (matching the
// lifetime of the authentication ticket) or session-based.
//IssuedUtc = <DateTimeOffset>,
// The time at which the authentication ticket was issued.
//RedirectUri = <string>
// The full path or absolute URI to be used as an http
// redirect response value.
};
//ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
//IsPersistent = true,
// Whether the authentication session is persisted across
// multiple requests. When used with cookies, controls
// whether the cookie's lifetime is absolute (matching the
// lifetime of the authentication ticket) or session-based.
//IssuedUtc = <DateTimeOffset>,
// The time at which the authentication ticket was issued.
//RedirectUri = <string>
// The full path or absolute URI to be used as an http
// redirect response value.
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return RedirectToAction("Index", "Startup");
}
ViewBag.ErrorTitle = $"Email claim not received from: Microsoft";
ViewBag.ErrorMessage = "Please contact support on info@dotnettutorials.net";
return View("Error");
}
ViewBag.ErrorMessage = "Please contact support on info@dotnettutorials.net";
return View("Error");
}
[HttpGet]
public async Task<IActionResult> Logout()

View File

@ -2,19 +2,11 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Facebook;
using Microsoft.AspNetCore.Mvc;
using Postall.Domain.Services.Contracts;
using System.Security.Claims;
namespace Postall.Controllers
{
public class OtherLoginsController : Controller
{
private readonly IFacebookServices _facebookServices;
public OtherLoginsController(IFacebookServices facebookServices)
{
this._facebookServices = facebookServices;
}
[HttpGet]
public IActionResult Index()
{
@ -32,13 +24,21 @@ namespace Postall.Controllers
public async Task<IActionResult> FacebookResponse()
{
var result = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme);
if (!result.Succeeded) return RedirectToAction("Login");
var accessToken = result.Properties.GetTokenValue("access_token");
var longLivedToken = await _facebookServices.GetLongLivedToken(accessToken);
if (!result.Succeeded)
return RedirectToAction("Login");
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
await _facebookServices.SaveFacebookToken(userId, longLivedToken);
var claims = result.Principal.Identities.FirstOrDefault()
.Claims.Select(claim => new
{
claim.Issuer,
claim.OriginalIssuer,
claim.Type,
claim.Value
});
// Aqui você pode implementar sua lógica de login
// Por exemplo, criar ou atualizar o usuário no banco de dados
return RedirectToAction("Index", "Home");
}

View File

@ -1,91 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Postall.Models;
namespace Postall.Controllers
{
public class SocialMediaController : Controller
{
[HttpGet]
public IActionResult Index()
{
// Implementar lógica para buscar lista de posts
return View();
}
[HttpGet]
public JsonResult GetPostDetails(int postId)
{
try
{
// Implementar lógica para buscar detalhes do post
return Json(new { success = true });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpGet]
public JsonResult GetSocialMediaStatus(int postId)
{
try
{
// Implementar lógica para buscar status das redes sociais
return Json(new { success = true });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpGet]
public IActionResult Post()
{
return View();
}
[HttpPost]
public JsonResult SaveDraft([FromBody] PostViewModel model)
{
try
{
// Lógica para salvar rascunho
return Json(new { success = true });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpPost]
public JsonResult PublishPost([FromBody] PostViewModel model)
{
try
{
// Lógica para publicar post
return Json(new { success = true });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpPost]
public JsonResult SchedulePost([FromBody] PostScheduleViewModel model)
{
try
{
// Lógica para agendar post
return Json(new { success = true });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
}
}

View File

@ -1,75 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Postall.Models;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Collections.Generic;
using Postall.Models;
namespace Postall.Controllers
{
[Authorize]
public class VideosController : Controller
{
// Simulação de dados para exemplo - em produção, estes dados viriam de uma API ou banco de dados
private List<VideoViewModel> GetSampleVideos()
{
return new List<VideoViewModel>
{
new VideoViewModel
{
Id = "video1",
Title = "Como usar o PostAll - Tutorial",
Description = "Aprenda a usar todas as funcionalidades do PostAll para gerenciar suas redes sociais.",
ThumbnailUrl = "https://i.ytimg.com/vi/sample1/maxresdefault.jpg",
PublishedAt = DateTime.Now.AddDays(-2)
},
new VideoViewModel
{
Id = "video2",
Title = "Estratégias de Marketing Digital para 2024",
Description = "Conheça as melhores estratégias para alavancar seu negócio nas redes sociais em 2024.",
ThumbnailUrl = "https://i.ytimg.com/vi/sample2/maxresdefault.jpg",
PublishedAt = DateTime.Now.AddDays(-5)
},
new VideoViewModel
{
Id = "video3",
Title = "Análise de Métricas nas Redes Sociais",
Description = "Aprenda a interpretar as métricas das suas redes sociais e tomar decisões baseadas em dados.",
ThumbnailUrl = "https://i.ytimg.com/vi/sample3/maxresdefault.jpg",
PublishedAt = DateTime.Now.AddDays(-7)
}
};
}
public IActionResult Index()
{
var userVideos = GetSampleVideos();
return View(userVideos);
}
[HttpGet]
public IActionResult GetChannelVideos()
{
// Em um cenário real, você buscaria os vídeos da API do YouTube
var channelVideos = GetSampleVideos().OrderByDescending(v => v.PublishedAt).Take(10).ToList();
return PartialView("_ChannelVideosPartial", channelVideos);
}
[HttpPost]
public IActionResult AddVideos(string[] selectedVideos)
{
if (selectedVideos == null || selectedVideos.Length == 0)
{
return RedirectToAction("Index");
}
// Em um cenário real, você salvaria esses IDs no banco de dados
// Aqui apenas redirecionamos de volta para o Index
TempData["Message"] = $"{selectedVideos.Length} vídeo(s) adicionado(s) com sucesso!";
return RedirectToAction("Index");
}
}
}

View File

@ -1,12 +0,0 @@
namespace Postall.Models
{
public class PostListViewModel
{
public int Id { get; set; }
public string Channel { get; set; }
public string Title { get; set; }
public DateTime? NextScheduledDate { get; set; }
public DateTime LastUpdate { get; set; }
public List<SocialMediaStatusViewModel> SocialMediaStatus { get; set; }
}
}

View File

@ -1,9 +0,0 @@
namespace Postall.Models
{
public class PostScheduleViewModel
{
public bool IsManual { get; set; }
public DayOfWeek? WeekDay { get; set; }
public TimeSpan? Time { get; set; }
}
}

View File

@ -1,10 +0,0 @@
namespace Postall.Models
{
public class PostViewModel
{
public string Title { get; set; }
public string Content { get; set; }
public string ImageUrl { get; set; }
public List<string> SelectedPlatforms { get; set; }
}
}

View File

@ -1,9 +0,0 @@
namespace Postall.Models
{
public class SocialMediaStatusViewModel
{
public string Platform { get; set; }
public DateTime? NextScheduledDate { get; set; }
public string Status { get; set; }
}
}

View File

@ -1,21 +0,0 @@
using System;
namespace Postall.Models
{
public class VideoViewModel
{
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string ThumbnailUrl { get; set; }
public DateTime PublishedAt { get; set; }
public bool IsSelected { get; set; }
// Propriedades adicionais que podem ser úteis no futuro
public string ChannelTitle { get; set; }
public string ChannelId { get; set; }
public string VideoUrl => $"https://www.youtube.com/watch?v={Id}";
}
}

View File

@ -32,12 +32,6 @@
<Folder Include="wwwroot\img\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
<ProjectReference Include="..\Postall.Infra.MongoDB\Postall.Infra.MongoDB.csproj" />
<ProjectReference Include="..\Postall.Infra\Postall.Infra.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resource.Designer.cs">
<DesignTime>True</DesignTime>

View File

@ -1,20 +1,15 @@
using BaseDomain.Extensions;
using Blinks.LogConfig;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Options;
using Postall.Domain.Services.Contracts;
using Postall.Infra.Services;
using Serilog;
using Serilog.Sinks.Grafana.Loki;
using Stripe;
using Stripe.Forwarding;
using System.Globalization;
using System.Security.Policy;
using Postall.Infra.MongoDB.Extensions;
using Postall.Infra.MongoDB.Settings;
var builder = WebApplication.CreateBuilder(args);
@ -91,15 +86,6 @@ builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient();
builder.Services.AddSerilog();
builder.Services.AddYouTubeServices();
builder.Services.AddRepositories();
builder.Services.AddScoped<IFacebookServices, FacebookTokenService>();
builder.Services.Configure<MongoDbSettings>(
builder.Configuration.GetSection("MongoDbSettings"));
var app = builder.Build();
var locOptions = app.Services.GetService<IOptions<RequestLocalizationOptions>>();

View File

@ -16,7 +16,7 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7078"
"applicationUrl": "https://localhost:7078;http://localhost:5094"
},
"IIS Express": {
"commandName": "IISExpress",

View File

@ -1,167 +0,0 @@
@{
ViewData["Title"] = "Adicionar Canal";
}
<div class="container mt-4">
<h2>Adicionar Canal</h2>
<div class="row mt-4">
<div class="col-md-8">
@if (TempData["Error"] != null)
{
<div class="alert alert-danger">
@TempData["Error"]
</div>
}
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="channelAddTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="search-tab" data-toggle="tab" href="#search" role="tab" aria-controls="search" aria-selected="true">
Buscar Canal
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="id-tab" data-toggle="tab" href="#id" role="tab" aria-controls="id" aria-selected="false">
Inserir ID do Canal
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content" id="channelAddTabsContent">
<div class="tab-pane fade show active" id="search" role="tabpanel" aria-labelledby="search-tab">
<div class="form-group">
<label for="channelSearch">Buscar Canal</label>
<div class="input-group">
<input type="text" class="form-control" id="channelSearch" placeholder="Digite o nome do canal..." minlength="3">
<div class="input-group-append">
<button class="btn btn-primary" type="button" id="searchButton">
<i class="bi bi-search"></i> Buscar
</button>
</div>
</div>
<small class="form-text text-muted">Insira pelo menos 3 caracteres para buscar.</small>
</div>
<div id="searchResults" class="mt-4"></div>
</div>
<div class="tab-pane fade" id="id" role="tabpanel" aria-labelledby="id-tab">
<form asp-action="Add" method="post">
<div class="form-group">
<label for="channelId">ID do Canal</label>
<input type="text" class="form-control" id="channelId" name="channelId" required
placeholder="Ex: UCkw4JCwteGrDHIsyIIKo4tQ">
<small class="form-text text-muted">
O ID do canal pode ser encontrado na URL do canal, ex: youtube.com/channel/<strong>UCkw4JCwteGrDHIsyIIKo4tQ</strong>
</small>
</div>
<button type="submit" class="btn btn-primary">Adicionar Canal</button>
</form>
</div>
</div>
</div>
</div>
<div class="mt-3">
<a href="@Url.Action("Index", "Channels")" class="btn btn-link">
<i class="bi bi-arrow-left"></i> Voltar para Meus Canais
</a>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title">Dica</h5>
<p class="card-text">
Adicione os canais do YouTube que você gerencia ou tem permissão para acessar.
Depois de adicionar os canais, você poderá ver os vídeos deles na seção "Vídeos".
</p>
<h6 class="mt-3">Como encontrar o ID do canal?</h6>
<ol class="small pl-3">
<li>Visite o canal no YouTube</li>
<li>Observe a URL: <code>youtube.com/channel/ID-DO-CANAL</code></li>
<li>Copie o ID que aparece após "/channel/"</li>
</ol>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(function() {
$('#searchButton').click(function() {
searchChannels();
});
$('#channelSearch').keypress(function(e) {
if (e.which === 13) {
e.preventDefault();
searchChannels();
}
});
function searchChannels() {
var query = $('#channelSearch').val().trim();
if (query.length < 3) {
alert('Por favor, insira pelo menos 3 caracteres para buscar.');
return;
}
$('#searchResults').html('<div class="text-center"><div class="spinner-border text-primary" role="status"></div><p class="mt-2">Buscando canais...</p></div>');
$.ajax({
url: '@Url.Action("Search", "Channels")',
type: 'GET',
data: { query: query },
success: function(response) {
if (response.success) {
displaySearchResults(response.data);
} else {
$('#searchResults').html('<div class="alert alert-danger">' + response.message + '</div>');
}
},
error: function() {
$('#searchResults').html('<div class="alert alert-danger">Erro ao buscar canais. Tente novamente.</div>');
}
});
}
function displaySearchResults(channels) {
if (!channels || channels.length === 0) {
$('#searchResults').html('<div class="alert alert-info">Nenhum canal encontrado com este termo.</div>');
return;
}
var html = '<h5>Resultados da busca</h5><div class="list-group">';
channels.forEach(function(channel) {
html += '<div class="list-group-item list-group-item-action">' +
'<div class="d-flex w-100">' +
'<div class="mr-3"><img src="' + channel.thumbnailUrl + '" alt="' + channel.title + '" width="50" class="rounded"></div>' +
'<div>' +
'<h6 class="mb-1">' + channel.title + '</h6>' +
'<p class="mb-1 small text-muted">' + (channel.description ? (channel.description.substring(0, 100) + '...') : '') + '</p>' +
'<form action="@Url.Action("Add", "Channels")" method="post">' +
'<input type="hidden" name="channelId" value="' + channel.id + '">' +
'<div class="d-flex justify-content-between align-items-center mt-2">' +
'<small class="text-muted">ID: ' + channel.id + '</small><br/>' +
'<button type="submit" class="btn btn-sm btn-primary">Adicionar</button>' +
'</div>' +
'</form>' +
'</div>' +
'</div>' +
'</div>';
});
html += '</div>';
$('#searchResults').html(html);
}
});
</script>
}

View File

@ -1,93 +0,0 @@
@using Postall.Domain.Dtos
@model List<ChannelResponse>
@{
ViewData["Title"] = "Meus Canais";
}
<div class="container mt-4">
<div class="row mb-4">
<div class="col-md-8">
<h2>Meus Canais</h2>
<p class="text-muted">Gerencie seus canais do YouTube</p>
</div>
<div class="col-md-4 text-right">
<a href="@Url.Action("Add", "Channels")" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Adicionar Canal
</a>
</div>
</div>
@if (TempData["Message"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["Message"]
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@TempData["Error"]
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
}
<div class="row">
@if (Model != null && Model.Any())
{
foreach (var channel in Model)
{
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<img src="@channel.ThumbnailUrl" alt="@channel.Title" class="img-fluid rounded">
</div>
<div class="col-md-8">
<h5 class="card-title">@channel.Title</h5>
<p class="card-text text-muted small">
@(channel.Description?.Length > 100 ? channel.Description.Substring(0, 100) + "..." : channel.Description)
</p>
<div class="d-flex justify-content-between">
<small class="text-muted">
<i class="bi bi-people"></i> @channel.SubscriberCount.ToString("N0") inscritos
</small>
<small class="text-muted">
<i class="bi bi-collection-play"></i> @channel.VideoCount.ToString("N0") vídeos
</small>
</div>
</div>
</div>
</div>
<div class="card-footer bg-white d-flex justify-content-between">
<a href="@channel.ChannelUrl" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-youtube"></i> Ver no YouTube
</a>
<form asp-action="Remove" asp-controller="Channels" method="post"
onsubmit="return confirm('Tem certeza que deseja remover este canal da sua lista?');">
<input type="hidden" name="id" value="@channel.Id" />
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> Remover
</button>
</form>
</div>
</div>
</div>
}
}
else
{
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Você ainda não possui canais. Clique em "Adicionar Canal" para começar.
</div>
</div>
}
</div>
</div>

View File

@ -1,54 +1,16 @@
@{
ViewData["Title"] = "Sites";
ViewData["Title"] = "Site";
}
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100">
<div class="col-md-6">
<div class="card shadow-lg">
<div class="card-body text-center p-5">
<h2 class="card-title mb-4">Bem-vindo</h2>
<p class="card-text text-muted mb-4">
Faça login com sua conta Microsoft para acessar o chat.
</p>
<form asp-action="FacebookLogin" method="get">
<button type="submit" class="btn btn-primary btn-block">
<i class="fab fa-facebook"></i> Login com Facebook
</button>
</form>
</div>
</div>
<div class="text-center">
<h1 class="display-4">Login</h1>
<div class="row justify-content-center">
<div class="col-md-4">
<form asp-action="FacebookLogin" method="get">
<button type="submit" class="btn btn-primary btn-block">
<i class="fab fa-facebook"></i> Login com Facebook
</button>
</form>
</div>
</div>
</div>
@section Styles {
<style>
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.card {
border: none;
border-radius: 15px;
}
.btn-primary {
background-color: #2f2f2f;
border: none;
padding: 12px;
transition: all 0.3s;
}
.btn-primary:hover {
background-color: #404040;
transform: translateY(-2px);
}
</style>
}
@section Scripts {
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
}

View File

@ -12,30 +12,6 @@
<link rel="stylesheet" href="~/Postall.styles.css" asp-append-version="true" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@await RenderSectionAsync("Styles", required: false)
<style>
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading .spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
</style>
</head>
<body class="hide-body">
<partial name="_Busy" />
@ -50,24 +26,12 @@
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">Planos</a>
</li>
@if (User!=null && User.Identity!=null && User.Identity.IsAuthenticated)
{
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Channels" asp-action="Index">Canais</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Videos" asp-action="Index">Videos</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="OtherLogins" asp-action="Index">Sites</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="SocialMedia" asp-action="Index">Postagens</a>
</li>
}
</ul>
@if (User!=null && User.Identity!=null && !User.Identity.IsAuthenticated)
{
@ -80,6 +44,11 @@
}
else
{
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="OtherLogins" asp-action="Index">Sites</a>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<partial name="_Language"/>
<li class="nav-item dropdown" style="margin-right: 10px">
@ -129,26 +98,8 @@
$(function () {
$(document).ready(function () {
$('.loading').hide();
$('#wrapper').show();
//$('body').fadeIn(1000);
$('body').slideDown('slow');
// Intercepta cliques em links
$('a:not([target="_blank"]):not([href^="#"]):not([href^="javascript"])').click(function(e) {
if (this.hostname === window.location.hostname) {
e.preventDefault();
const href = $(this).attr('href');
// Adiciona fade out
$('#wrapper').addClass('fade-out');
$('.loading').show();
// Navega após a animação
setTimeout(() => {
window.location.href = href;
}, 300);
}
});
});
$('a[href="#search"]').on('click', function (event) {
@ -177,7 +128,7 @@
$(document).ready(function () {
$('#wrapper').fadeIn('slow');
$('#navbarNav a.nav-link').click(function () {
$('a.nav-link').click(function () {
if ($(this).hasClass('dropdown-toggle')) {
return;
}
@ -229,19 +180,6 @@
$('body').slideUp('slow');
}
// Remove fade-out quando a página carrega
$(window).on('pageshow', function() {
$('#wrapper').removeClass('fade-out');
$('.loading').hide();
setActiveByLocation();
});
// Gerencia submissão de formulários
$(document).on('submit', 'form', function () {
$('#wrapper').addClass('fade-out');
$('.loading').show();
});
//$(".hide-body").fadeOut(2000);
});
</script>

View File

@ -1,254 +0,0 @@
@using Postall.Models
<!-- Views/SocialMedia/Index.cshtml -->
@{
ViewData["Title"] = "Gerenciador de Postagens";
}
<div class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Gerenciador de Postagens</h2>
<a href="@Url.Action("Post", "SocialMedia")" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Nova Postagem
</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="postsTable">
<thead>
<tr>
<th></th>
<th>Canal</th>
<th>Título</th>
<th>Próxima Data</th>
<th>Última Atualização</th>
</tr>
</thead>
<tbody>
<!-- Os dados serão preenchidos via JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Template para detalhes expandidos -->
<template id="detailsTemplate">
<div class="expanded-details p-3">
<div class="row">
<div class="col-md-8">
<h5 class="mb-3">Status das Redes Sociais</h5>
<div class="social-media-grid">
<!-- Facebook -->
<div class="social-media-item">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-facebook me-2"></i>
<span class="platform-name">Facebook</span>
</div>
<div class="status-badge">
<span class="badge rounded-pill status-placeholder">Status</span>
</div>
<div class="next-date small text-muted mt-1">
Próxima data: <span class="date-placeholder">--/--/----</span>
</div>
</div>
<!-- Instagram -->
<div class="social-media-item">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-instagram me-2"></i>
<span class="platform-name">Instagram</span>
</div>
<div class="status-badge">
<span class="badge rounded-pill status-placeholder">Status</span>
</div>
<div class="next-date small text-muted mt-1">
Próxima data: <span class="date-placeholder">--/--/----</span>
</div>
</div>
<!-- Twitter/X -->
<div class="social-media-item">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-twitter-x me-2"></i>
<span class="platform-name">Twitter/X</span>
</div>
<div class="status-badge">
<span class="badge rounded-pill status-placeholder">Status</span>
</div>
<div class="next-date small text-muted mt-1">
Próxima data: <span class="date-placeholder">--/--/----</span>
</div>
</div>
<!-- WhatsApp -->
<div class="social-media-item">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-whatsapp me-2"></i>
<span class="platform-name">WhatsApp</span>
</div>
<div class="status-badge">
<span class="badge rounded-pill status-placeholder">Status</span>
</div>
<div class="next-date small text-muted mt-1">
Próxima data: <span class="date-placeholder">--/--/----</span>
</div>
</div>
<!-- Telegram -->
<div class="social-media-item">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-telegram me-2"></i>
<span class="platform-name">Telegram</span>
</div>
<div class="status-badge">
<span class="badge rounded-pill status-placeholder">Status</span>
</div>
<div class="next-date small text-muted mt-1">
Próxima data: <span class="date-placeholder">--/--/----</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@section Styles {
<style>
.social-media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.social-media-item {
padding: 1rem;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
background-color: #f8f9fa;
}
.status-badge .badge {
font-size: 0.875rem;
}
.badge.status-publicado { background-color: #198754; }
.badge.status-nao-selecionado { background-color: #6c757d; }
.badge.status-gerado { background-color: #0dcaf0; }
.badge.status-agendado { background-color: #ffc107; color: #000; }
.badge.status-automatizado { background-color: #0d6efd; }
tr.expanded {
background-color: #f8f9fa;
}
.expanded-details {
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
}
@@media (max-width: 768px) {
.social-media-grid {
grid-template-columns: 1fr;
}
}
</style>
}
@section Scripts {
<script>
$(document).ready(function() {
loadPosts();
});
function loadPosts() {
$.get('@Url.Action("GetPostDetails", "SocialMedia")', function(data) {
if (data.success) {
renderPosts(data.posts);
}
});
}
function renderPosts(posts) {
const tbody = $('#postsTable tbody');
tbody.empty();
posts.forEach(post => {
const row = $(`
<tr data-post-id="${post.id}">
<td>
<button class="btn btn-sm btn-link expand-btn">
<i class="bi bi-chevron-right"></i>
</button>
</td>
<td>${post.channel}</td>
<td>${post.title}</td>
<td>${formatDate(post.nextScheduledDate)}</td>
<td>${formatDate(post.lastUpdate)}</td>
</tr>
`);
tbody.append(row);
});
}
function formatDate(dateString) {
if (!dateString) return '--/--/----';
const date = new Date(dateString);
return date.toLocaleDateString('pt-BR');
}
function getStatusBadgeClass(status) {
const statusMap = {
'publicado': 'status-publicado',
'não selecionado': 'status-nao-selecionado',
'gerado': 'status-gerado',
'agendado': 'status-agendado',
'automatizado': 'status-automatizado'
};
return statusMap[status.toLowerCase()] || '';
}
$(document).on('click', '.expand-btn', function() {
const row = $(this).closest('tr');
const postId = row.data('post-id');
const icon = $(this).find('i');
if (row.next().hasClass('details-row')) {
// Fechar detalhes
row.next().remove();
row.removeClass('expanded');
icon.removeClass('bi-chevron-down').addClass('bi-chevron-right');
} else {
// Abrir detalhes
$.get(`@Url.Action("GetSocialMediaStatus", "SocialMedia")?postId=${postId}`, function(data) {
if (data.success) {
const template = document.getElementById('detailsTemplate');
const detailsContent = template.content.cloneNode(true);
// Preencher os status
data.socialMediaStatus.forEach(status => {
const platformElement = $(detailsContent).find(`[data-platform="${status.platform}"]`);
platformElement.find('.status-placeholder')
.text(status.status)
.addClass(getStatusBadgeClass(status.status));
platformElement.find('.date-placeholder').text(formatDate(status.nextScheduledDate));
});
const detailsRow = $('<tr class="details-row">').append(
$('<td colspan="5">').append(detailsContent)
);
row.addClass('expanded');
row.after(detailsRow);
icon.removeClass('bi-chevron-right').addClass('bi-chevron-down');
}
});
}
});
</script>
}

View File

@ -1,347 +0,0 @@
@using Postall.Models
@model PostViewModel
@{
ViewData["Title"] = "Gerenciar Postagens";
}
<div class="container-fluid">
<div class="row">
<!-- Menu lateral em desktop / Menu superior em mobile -->
<div class="col-md-3 col-12 mb-3">
<div class="card">
<div class="card-header">
<h5>Plataformas</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="facebook">
Facebook
</button>
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="instagram">
Instagram
</button>
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="twitter">
Twitter/X
</button>
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="whatsapp">
WhatsApp
</button>
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="telegram">
Telegram
</button>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5>Agendamento</h5>
</div>
<div class="card-body">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="schedule" id="manual" checked>
<label class="form-check-label" for="manual">Manual</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="schedule" id="weekly">
<label class="form-check-label" for="weekly">Semanal</label>
</div>
<div id="weeklyOptions" class="d-none">
<div class="mb-2">
<div class="form-check">
<input class="form-check-input weekday-check" type="checkbox" value="1" id="monday">
<label class="form-check-label" for="monday">Segunda-feira</label>
</div>
<div class="form-check">
<input class="form-check-input weekday-check" type="checkbox" value="2" id="tuesday">
<label class="form-check-label" for="tuesday">Terça-feira</label>
</div>
<div class="form-check">
<input class="form-check-input weekday-check" type="checkbox" value="3" id="wednesday">
<label class="form-check-label" for="wednesday">Quarta-feira</label>
</div>
<div class="form-check">
<input class="form-check-input weekday-check" type="checkbox" value="4" id="thursday">
<label class="form-check-label" for="thursday">Quinta-feira</label>
</div>
<div class="form-check">
<input class="form-check-input weekday-check" type="checkbox" value="5" id="friday">
<label class="form-check-label" for="friday">Sexta-feira</label>
</div>
<div class="form-check">
<input class="form-check-input weekday-check" type="checkbox" value="6" id="saturday">
<label class="form-check-label" for="saturday">Sábado</label>
</div>
<div class="form-check">
<input class="form-check-input weekday-check" type="checkbox" value="0" id="sunday">
<label class="form-check-label" for="sunday">Domingo</label>
</div>
</div>
<input type="time" class="form-control" id="scheduleTime">
</div>
</div>
</div>
</div>
<!-- Preview lateral -->
<div class="col-md-6 col-12">
<div class="card">
<div class="card-header">
<h5>Video</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Canal</label>
<input type="text" class="form-control" id="previewChannel" readonly>
</div>
<div class="mb-3">
<label class="form-label">URL do Vídeo</label>
<input type="text" class="form-control" id="previewVideoUrl">
</div>
<div class="preview-content">
<h6>Título</h6>
<p id="previewTitle"></p>
<h6>Conteúdo</h6>
<p id="previewContent"></p>
<div id="previewImage" class="mt-2"></div>
</div>
</div>
<div class="card-footer">
<button type="button" class="btn btn-secondary me-2" onclick="savePlatformDraft()">Gerar com IA</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h5>Nova Postagem</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="postTitle" class="form-label">Título</label>
<input type="text" class="form-control" id="postTitle">
</div>
<div class="mb-3">
<label for="postContent" class="form-label">Conteúdo</label>
<textarea class="form-control" id="postContent" rows="5"></textarea>
</div>
<div class="mb-3">
<label for="postImage" class="form-label">Imagem</label>
<input type="file" class="form-control" id="postImage" accept="image/*">
</div>
<div class="preview-image mb-3 d-none">
<img id="imagePreview" src="#" alt="Preview" class="img-fluid">
</div>
</div>
<div class="card-footer">
<button type="button" class="btn btn-secondary me-2" onclick="saveDraft()">Salvar Rascunho</button>
<button type="button" class="btn btn-primary" onclick="publishPost()">Publicar</button>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
let selectedPlatform = null;
// Gestão das plataformas
document.querySelectorAll('.platform-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const platform = this.dataset.platform;
// Reset outros botões
document.querySelectorAll('.platform-btn').forEach(b =>
b.classList.replace('btn-primary', 'btn-outline-primary'));
// Ativar botão selecionado
this.classList.replace('btn-outline-primary', 'btn-primary');
selectedPlatform = platform;
// Carregar dados da plataforma
await loadPlatformData(platform);
});
});
async function loadPlatformData(platform) {
try {
const response = await fetch(`/api/socialMedia/platformData/${platform}`);
const data = await response.json();
// Atualizar preview
document.getElementById('previewChannel').value = data.channel;
document.getElementById('previewVideoUrl').value = data.videoUrl;
document.getElementById('previewTitle').textContent = data.title;
document.getElementById('previewContent').textContent = data.content;
if (data.imageUrl) {
document.getElementById('previewImage').innerHTML =
`<img src="${data.imageUrl}" class="img-fluid" alt="Preview">`;
}
} catch (error) {
console.error('Erro ao carregar dados da plataforma:', error);
}
}
// Mostrar/ocultar opções de agendamento semanal
document.getElementById('weekly').addEventListener('change', function() {
document.getElementById('weeklyOptions').classList.remove('d-none');
});
document.getElementById('manual').addEventListener('change', function() {
document.getElementById('weeklyOptions').classList.add('d-none');
});
// Preview da imagem
document.getElementById('postImage').addEventListener('change', function(e) {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('imagePreview').src = e.target.result;
document.querySelector('.preview-image').classList.remove('d-none');
};
reader.readAsDataURL(e.target.files[0]);
}
});
function getSelectedPlatform() {
return selectedPlatform;
}
function getSelectedWeekDays() {
return Array.from(document.querySelectorAll('.weekday-check:checked'))
.map(checkbox => parseInt(checkbox.value));
}
async function generateAIContent() {
if (!selectedPlatform) {
alert('Selecione uma plataforma primeiro!');
return;
}
try {
const response = await fetch('/api/socialMedia/generateContent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform: selectedPlatform,
videoUrl: document.getElementById('previewVideoUrl').value
})
});
const data = await response.json();
if (data.success) {
document.getElementById('previewTitle').textContent = data.title;
document.getElementById('previewContent').textContent = data.content;
} else {
alert('Erro ao gerar conteúdo: ' + data.message);
}
} catch (error) {
console.error('Erro ao gerar conteúdo:', error);
alert('Erro ao gerar conteúdo');
}
}
async function savePlatformDraft() {
if (!selectedPlatform) {
alert('Selecione uma plataforma primeiro!');
return;
}
const data = {
platform: selectedPlatform,
title: document.getElementById('previewTitle').textContent,
content: document.getElementById('previewContent').textContent,
videoUrl: document.getElementById('previewVideoUrl').value
};
try {
const response = await fetch('/api/socialMedia/savePlatformDraft', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
alert('Rascunho salvo com sucesso!');
} else {
alert('Erro ao salvar rascunho: ' + result.message);
}
} catch (error) {
console.error('Erro ao salvar rascunho:', error);
alert('Erro ao salvar rascunho');
}
}
function publishPost() {
const isWeekly = document.getElementById('weekly').checked;
const data = {
title: document.getElementById('postTitle').value,
content: document.getElementById('postContent').value,
platform: getSelectedPlatform()
};
if (isWeekly) {
data.isManual = false;
data.weekDays = getSelectedWeekDays();
data.time = document.getElementById('scheduleTime').value;
$.ajax({
url: '@Url.Action("SchedulePost", "SocialMedia")',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function(response) {
if (response.success) {
alert('Post agendado com sucesso!');
} else {
alert('Erro ao agendar post: ' + response.message);
}
}
});
} else {
$.ajax({
url: '@Url.Action("PublishPost", "SocialMedia")',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function(response) {
if (response.success) {
alert('Post publicado com sucesso!');
} else {
alert('Erro ao publicar post: ' + response.message);
}
}
});
}
}
</script>
}
<style>
.platform-btn {
min-width: 120px;
margin-bottom: 8px;
}
.preview-image img {
max-height: 300px;
object-fit: contain;
}
@@media (max-width: 768px) {
.container-fluid {
padding: 10px;
}
}
.card {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
</style>

View File

@ -1,169 +0,0 @@
@model List<Postall.Models.VideoViewModel>
@{
ViewData["Title"] = "Gerenciador de Vídeos";
}
<div class="container mt-4">
<div class="row mb-4">
<div class="col-md-8">
<h2>Meus Vídeos</h2>
<p class="text-muted">Gerencie seus vídeos do YouTube</p>
</div>
<div class="col-md-4 text-right">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addVideosModal">
<i class="bi bi-plus-circle"></i> Adicionar Vídeos
</button>
</div>
</div>
@if (TempData["Message"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["Message"]
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
}
<div class="row">
@if (Model != null && Model.Any())
{
foreach (var video in Model)
{
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header bg-transparent">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">@video.Title</h5>
<button class="btn btn-sm btn-link" type="button" data-toggle="collapse"
data-target="#collapse-@video.Id" aria-expanded="false">
<i class="bi bi-chevron-down"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-5">
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
</div>
<div class="col-md-7">
<p class="card-text">@(video.Description?.Length > 100 ? video.Description.Substring(0, 100) + "..." : video.Description)</p>
<p class="text-muted small">Publicado em: @video.PublishedAt.ToString("dd/MM/yyyy")</p>
</div>
</div>
</div>
<div class="collapse" id="collapse-@video.Id">
<div class="card-footer bg-white">
<div class="d-flex justify-content-between">
<div>
<a href="@video.VideoUrl" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-youtube"></i> Ver no YouTube
</a>
</div>
<div>
<button class="btn btn-sm btn-outline-secondary mr-1">
<i class="bi bi-pencil"></i> Editar
</button>
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> Remover
</button>
</div>
</div>
</div>
</div>
</div>
</div>
}
}
else
{
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Você ainda não possui vídeos. Clique em "Adicionar Vídeos" para começar.
</div>
</div>
}
</div>
</div>
<!-- Modal para adicionar vídeos -->
<div class="modal fade" id="addVideosModal" tabindex="-1" role="dialog" aria-labelledby="addVideosModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addVideosModalLabel">Adicionar Vídeos do YouTube</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="channelVideosContainer">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Carregando...</span>
</div>
<p class="mt-2">Carregando vídeos...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" id="btnAddSelectedVideos">Adicionar Selecionados</button>
</div>
</div>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(function () {
// Carrega os vídeos do canal quando o modal é aberto
$('#addVideosModal').on('shown.bs.modal', function () {
loadChannelVideos();
});
// Função para carregar os vídeos do canal
function loadChannelVideos() {
$.ajax({
url: '@Url.Action("GetChannelVideos", "Videos")',
type: 'GET',
success: function (result) {
$('#channelVideosContainer').html(result);
},
error: function (error) {
$('#channelVideosContainer').html('<div class="alert alert-danger">Erro ao carregar vídeos. Tente novamente.</div>');
console.error('Erro:', error);
}
});
}
// Manipula o clique no botão de adicionar vídeos selecionados
$('#btnAddSelectedVideos').click(function () {
var selectedVideos = [];
// Coleta todos os checkboxes selecionados
$('.video-checkbox:checked').each(function () {
selectedVideos.push($(this).val());
});
if (selectedVideos.length === 0) {
alert('Selecione pelo menos um vídeo para adicionar.');
return;
}
// Cria um formulário para enviar os IDs dos vídeos selecionados
var form = $('<form action="@Url.Action("AddVideos", "Videos")" method="post"></form>');
// Adiciona inputs ocultos para cada vídeo selecionado
selectedVideos.forEach(function (videoId) {
form.append('<input type="hidden" name="selectedVideos" value="' + videoId + '">');
});
// Adiciona o formulário ao corpo do documento e o submete
$('body').append(form);
form.submit();
});
});
</script>
}

View File

@ -1,51 +0,0 @@
@model List<Postall.Models.VideoViewModel>
<form id="channelVideosForm">
<h6 class="mb-3">Últimos 10 vídeos disponíveis (ordenados por data):</h6>
@if (Model != null && Model.Any())
{
<div class="list-group">
@foreach (var video in Model)
{
<div class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex">
<div class="checkbox-container mr-3 mt-1">
<div class="form-check">
<input class="form-check-input video-checkbox" type="checkbox" value="@video.Id" id="video-@video.Id">
</div>
</div>
<div class="row w-100">
<div class="col-md-4">
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
</div>
<div class="col-md-8">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">@video.Title</h5>
<small>@video.PublishedAt.ToString("dd/MM/yyyy")</small>
</div>
<p class="mb-1">@video.Description</p>
<small class="text-muted">
<i class="bi bi-youtube"></i>
@(video.ChannelTitle ?? "Seu Canal")
</small>
</div>
</div>
</div>
</div>
}
</div>
}
else
{
<div class="alert alert-info">
Nenhum vídeo encontrado nos canais aos quais você tem acesso.
</div>
}
</form>
<style>
.checkbox-container {
min-width: 30px;
}
</style>

View File

@ -4,10 +4,5 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "YouTubeChannelsDB",
"ChannelsCollectionName": "Channels"
}
}

View File

@ -18,16 +18,8 @@
"AppSecret": "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"
},
"Facebook": {
"AppId": "963281005306692",
"AppSecret": "575839ccbb36d1457715f1f6dd0a8db9"
"AppId": "seu_app_id_aqui",
"AppSecret": "seu_app_secret_aqui"
}
},
"Youtube": {
"ApiKey": "AIzaSyBwFTW5WRAaBupyTgayVIeaS3LjGn0gpNI"
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "YouTubeChannelsDB",
"ChannelsCollectionName": "Channels"
}
}

View File

@ -1,82 +0,0 @@
/* Estilos para a página de vídeos */
.card {
transition: all 0.3s ease;
border: 1px solid rgba(0,0,0,.125);
}
.card:hover {
box-shadow: 0 5px 15px rgba(0,0,0,.1);
}
.card-header button.btn-link {
text-decoration: none;
color: #212529;
}
.card-header button.btn-link:hover {
color: #007bff;
}
.card-body img {
max-height: 120px;
object-fit: cover;
width: 100%;
}
.video-checkbox {
cursor: pointer;
width: 20px;
height: 20px;
}
.list-group-item {
transition: background-color 0.2s ease;
}
.list-group-item:hover {
background-color: rgba(0,123,255,.03);
}
.list-group-item img {
max-height: 100px;
object-fit: cover;
}
/* Animação para o colapso */
.collapse {
transition: all 0.3s ease;
}
/* Estilo para o botão de expansão do card */
.card-header button.btn-link {
transform: rotate(0deg);
transition: transform 0.3s ease;
}
.card-header button.btn-link[aria-expanded="true"] i {
transform: rotate(180deg);
}
/* Ajustes para responsividade */
@media (max-width: 768px) {
.col-md-6.mb-4 {
padding-left: 5px;
padding-right: 5px;
}
.card-body .row {
flex-direction: column;
}
.card-body .col-md-5,
.card-body .col-md-7 {
width: 100%;
max-width: 100%;
flex: 0 0 100%;
}
.card-body .col-md-5 {
margin-bottom: 15px;
}
}