Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Core.Application.DTO.PostAttachment.Response;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Application.DTO.LinkedIn.Request
{
public class LinkedInPostWithMediaRequest
{
public required string AccessToken { get; set; }
public required string ExternalAccountId { get; set; }
public required string Content { get; set; }
public List<PostAttachmentResponse> Attachments { get; set; } = [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ public class PostPublicationParamsRequest : PaginatedListQueryParams
public PostPublicationStatus? Status { get; set; }
public DateTime? PublishedBefore { get; set; }
public DateTime? PublishedAfter { get; set; }
public DateTime? CreatedBefore { get; set; }
public DateTime? CreatedAfter { get; set; }
public PlatformType? PublishedOn { get; set; }
public PostPublishSortBy? SortBy { get; set; }
}

public enum PostPublishSortBy
{
CreatedAt,
Platform,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ namespace Core.Application.DTO.PostPublication.Response
{
public class PostPublicationResponse
{
public int PostId { get; set; }
public int UserPlatformId { get; set; }
public string PostBody { get; set; }
public PlatformType PlatformType { get; set; }
public string AccountUsername { get; set; }
public PostPublicationStatus Status { get; set; }
public DateTime? PublishedAt { get; set; }
public string? ExternalPostId { get; set; }
Expand Down
1 change: 1 addition & 0 deletions Core.Application/Services/IPostPublishService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ namespace Core.Application.Services
public interface IPostPublishService
{
public Task<PostPublicationResponse> PublishPostToLinkedInAsync(PublishPostRequest request, string userId);
public Task<List<PostPublicationResponse>> GetUserPostPublicationsAsync(PostPublicationParamsRequest request, string userId);
}
}
2 changes: 1 addition & 1 deletion Core.Application/Services/IPostService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Core.Application.Services
public interface IPostService
{
public Task<List<PostResponse>> GetAllPostsAsync(PostPaginatedParamsRequest request);
public Task<List<PostResponse>> GetAllPostsPerUserAsync(PostPaginatedParamsRequest request, string userId);
public Task<List<PostResponseLong>> GetAllPostsPerUserAsync(PostPaginatedParamsRequest request, string userId);
public Task<PostResponse> GetUserPostById(int postId, string userId);
public Task<PostResponseLong> GetPostByIdLong(int postId, string userId);
public Task<PostResponse> CreatePostAsync(PostRequest request, string userId);
Expand Down
1 change: 1 addition & 0 deletions Core.Application/Services/LinkedIn/ILinkedInService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface ILinkedInService
Task<string> ExchangeCodeForAccessToken(string code, string redirectUri);
Task<LinkedInAccountInfoResponse> GetAccountInfo(string accessToken);
Task<string> PostTextAsync(LinkedInPostRequest request);
Task<string> PostTextWithMediaAsync(LinkedInPostWithMediaRequest request, string userId);
Task<List<LinkedInOrganizationsResponse>> GetUserCompanies(string accessToken);
}
}
2 changes: 2 additions & 0 deletions Repository/Repositories/PostPublicationRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public IQueryable<PostPublication> GetAllAsQueryPerUserIdAsync(string userId)
{
return dbContext.PostPublications
.Include(pp => pp.UserPlatform)
.ThenInclude(platform => platform.Platform)
.Include(pp => pp.Post)
.Where(pb => pb.UserPlatform.UserId == userId).AsNoTracking();
}

Expand Down
4 changes: 3 additions & 1 deletion Repository/Repositories/PostRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public IQueryable<Post> GetAllAsQueryAsync()

public IQueryable<Post> GetAllAsQueryPerUserIdAsync(string userId)
{
return dbContext.Posts.Where(p => p.UserId == userId).AsNoTracking();
return dbContext.Posts.
Include(p => p.Attachments)
.Where(p => p.UserId == userId).AsNoTracking();
}

public async Task<List<Post>> GetAllAsync()
Expand Down
198 changes: 188 additions & 10 deletions Service/LinkedIn/LinkedInService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
using Core.Application.DTO.LinkedIn;
using Core.Application.DTO.LinkedIn.Request;
using Core.Application.DTO.LinkedIn.Response;
using Core.Application.DTO.PostAttachment.Response;
using Core.Application.Services;
using Core.Domain.Entities;
using Microsoft.Extensions.Configuration;
using Service.Exceptions;
using Service.Services;
using System;
using System.Collections.Generic;
using System.Net.Http;
Expand All @@ -7,15 +15,12 @@
using System.Text.Json;
using System.Threading.Tasks;
using System.Xml.Linq;
using Core.Application.DTO.LinkedIn;
using Core.Application.DTO.LinkedIn.Request;
using Core.Application.DTO.LinkedIn.Response;
using Core.Application.Services;
using Service.Exceptions;

namespace Service.LinkedIn
{
public class LinkedInService(HttpClient client, LinkedInConfig enviromentalConfig, IConfiguration appsettingsConfig) : ILinkedInService
public class LinkedInService(HttpClient client, LinkedInConfig enviromentalConfig,
IUserUploadedFileService userUploadedFileService,
IConfiguration appsettingsConfig) : ILinkedInService
{

public async Task<string> ExchangeCodeForAccessToken(string code, string redirectUri)
Expand Down Expand Up @@ -153,14 +158,187 @@ public async Task<string> PostTextAsync(LinkedInPostRequest request)
throw new BadRequestException($"LinkedIn post failed: {resp.StatusCode} - {json}");
}

var response = JsonSerializer.Deserialize<LinkedInPostResponse>(json);
if (json.StartsWith("\""))
{
json = JsonSerializer.Deserialize<string>(json);
}

using var doc = JsonDocument.Parse(json);

var id = doc.RootElement.GetProperty("id").GetString();

return id;
}

public async Task<string> PostTextWithMediaAsync(LinkedInPostWithMediaRequest request, string userId)
{
var accessToken = request.AccessToken;
var authorUrn = $"urn:li:person:{request.ExternalAccountId}";

client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);

client.DefaultRequestHeaders.Remove("X-Restli-Protocol-Version");
client.DefaultRequestHeaders.Add("X-Restli-Protocol-Version", "2.0.0");

var media = await CreateMediaAsync(
request.Attachments,
authorUrn,
accessToken,
userId);

var body = new
{
author = authorUrn,
lifecycleState = "PUBLISHED",
specificContent = new
{
com_linkedin_ugc_ShareContent = new
{
shareCommentary = new
{
text = request.Content
},
shareMediaCategory = "IMAGE",
media
}
},
visibility = new
{
com_linkedin_ugc_MemberNetworkVisibility = "PUBLIC"
}
};

var raw = JsonSerializer.Serialize(body)
.Replace("com_linkedin_ugc_ShareContent", "com.linkedin.ugc.ShareContent")
.Replace("com_linkedin_ugc_MemberNetworkVisibility", "com.linkedin.ugc.MemberNetworkVisibility");

var resp = await client.PostAsync(
$"{appsettingsConfig["LinkedIn:ApiUrl"]}ugcPosts",
new StringContent(raw, Encoding.UTF8, "application/json"));

var json = await resp.Content.ReadAsStringAsync();

if(response == null)
if (!resp.IsSuccessStatusCode)
{
throw new BadRequestException($"LinkedIn post failed: {resp.StatusCode} - {json}");
}

if (json.StartsWith("\""))
{
throw new BadRequestException("Failed to parse LinkedIn post response.");
json = JsonSerializer.Deserialize<string>(json);
}

return response.Id;
using var doc = JsonDocument.Parse(json);

var id = doc.RootElement.GetProperty("id").GetString();

return id;
}


private async Task<List<object>> CreateMediaAsync(
IEnumerable<PostAttachmentResponse> attachments,
string ownerUrn,
string accessToken,
string userId)
{
var media = new List<object>();

foreach (var attachment in attachments)
{
var (asset, uploadUrl) = await RegisterUploadAsync(ownerUrn);

await UploadMediaAsync(uploadUrl, accessToken, attachment, userId);

media.Add(new
{
status = "READY",
media = asset
});
}

return media;
}


private async Task<(string Asset, string UploadUrl)> RegisterUploadAsync(string ownerUrn)
{
var body = new
{
registerUploadRequest = new
{
recipes = new[]
{
"urn:li:digitalmediaRecipe:feedshare-image"
},
owner = ownerUrn,
serviceRelationships = new[]
{
new
{
relationshipType = "OWNER",
identifier = "urn:li:userGeneratedContent"
}
}
}
};

var response = await client.PostAsync(
$"{appsettingsConfig["LinkedIn:ApiUrl"]}assets?action=registerUpload",
new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"));

var json = await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
throw new BadRequestException($"LinkedIn register upload failed: {json}");

using var document = JsonDocument.Parse(json);

var value = document.RootElement.GetProperty("value");

var asset = value.GetProperty("asset").GetString()!;

var uploadUrl = value
.GetProperty("uploadMechanism")
.GetProperty("com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest")
.GetProperty("uploadUrl")
.GetString()!;

return (asset, uploadUrl);
}


private async Task UploadMediaAsync(
string uploadUrl,
string accessToken,
PostAttachmentResponse attachment,
string userId)
{
var fileResult = await userUploadedFileService.DownloadFileById(attachment.UserUploadedFileId, userId);

fileResult.FileStream.Position = 0;

using var request = new HttpRequestMessage(HttpMethod.Post, uploadUrl);

request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);

request.Content = new StreamContent(fileResult.FileStream);

request.Content.Headers.ContentType =
new MediaTypeHeaderValue(fileResult.ContentType);

var response = await client.SendAsync(request);

if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new BadRequestException($"LinkedIn upload failed: {error}");
}
}

}


}
5 changes: 4 additions & 1 deletion Service/Mapping/AutoMapperProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ public AutoMapperProfile()

CreateMap<UserPrompt, UserPromptResponse>();

CreateMap<PostPublication, PostPublicationResponse>();
CreateMap<PostPublication, PostPublicationResponse>()
.ForMember(d => d.AccountUsername, o => o.MapFrom(s => s.UserPlatform.AccountUsername))
.ForMember(d => d.PostBody, o => o.MapFrom(s => s.Post.Body));

CreateMap<PublishPostRequest, PostPublication>();

CreateMap<UserUploadedFile, UserUploadedFileResponse>()
Expand Down
Loading
Loading