Skip to content

Commit

Permalink
Merge pull request #29 from a-gubskiy/27-implement-image-attachment-f…
Browse files Browse the repository at this point in the history
…or-posts

Close #27
Implemented image attachments for posts
  • Loading branch information
a-gubskiy authored Dec 12, 2024
2 parents 411b44f + a1dd71d commit 427233a
Show file tree
Hide file tree
Showing 18 changed files with 533 additions and 226 deletions.
8 changes: 4 additions & 4 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
<Copyright>Andrew Gubskiy © 2024</Copyright>
<Company>Ukrainian .NET Developer Community</Company>

<Version>1.1.7</Version>
<AssemblyVersion>1.1.7</AssemblyVersion>
<FileVersion>1.1.7</FileVersion>
<PackageVersion>1.1.7</PackageVersion>
<Version>1.3.0</Version>
<AssemblyVersion>1.3.0</AssemblyVersion>
<FileVersion>1.3.0</FileVersion>
<PackageVersion>1.3.0</PackageVersion>

<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/ernado-x/X.Bluesky.git</RepositoryUrl>
Expand Down
34 changes: 22 additions & 12 deletions src/X.Bluesky/AuthorizationClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,39 @@ public class AuthorizationClient : IAuthorizationClient
private readonly IHttpClientFactory _httpClientFactory;
private readonly string _identifier;
private readonly string _password;

/// <summary>
/// Session reuse flag
/// </summary>
private readonly bool _reuseSession;


private readonly Uri _baseUri;

private Session? _session;
private DateTime? _sessionRefreshedAt;

[PublicAPI]
public AuthorizationClient(string identifier, string password)
: this(new BlueskyHttpClientFactory(), identifier, password, false)
: this(new BlueskyHttpClientFactory(), identifier, password, false, new Uri("https://bsky.social"))
{
}

[PublicAPI]
public AuthorizationClient(string identifier, string password, bool reuseSession)
: this(new BlueskyHttpClientFactory(), identifier, password, reuseSession)
public AuthorizationClient(string identifier, string password, bool reuseSession, Uri baseUri)
: this(new BlueskyHttpClientFactory(), identifier, password, reuseSession, baseUri)
{
}

[PublicAPI]
public AuthorizationClient(IHttpClientFactory httpClientFactory, string identifier, string password, bool reuseSession)
public AuthorizationClient(
IHttpClientFactory httpClientFactory,
string identifier,
string password,
bool reuseSession,
Uri baseUri)
{
_reuseSession = reuseSession;
_baseUri = baseUri;
_httpClientFactory = httpClientFactory;
_identifier = identifier;
_password = password;
Expand All @@ -53,13 +61,14 @@ public AuthorizationClient(IHttpClientFactory httpClientFactory, string identifi
/// </returns>
public async Task<Session> GetSession()
{
if (_reuseSession && _session != null && _sessionRefreshedAt != null && _sessionRefreshedAt.Value.AddMinutes(90) > DateTime.UtcNow)
if (_reuseSession && _session != null
&& _sessionRefreshedAt != null
&& _sessionRefreshedAt.Value.AddMinutes(90) > DateTime.UtcNow)
{
// Reuse existing session

return _session;
}

var requestData = new
{
identifier = _identifier,
Expand All @@ -72,13 +81,14 @@ public async Task<Session> GetSession()

var httpClient = _httpClientFactory.CreateClient();

var uri = "https://bsky.social/xrpc/com.atproto.server.createSession";
var uri = $"{_baseUri.ToString().TrimEnd('/')}/xrpc/com.atproto.server.createSession";

var response = await httpClient.PostAsync(uri, content);

response.EnsureSuccessStatusCode();

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

_session = JsonConvert.DeserializeObject<Session>(jsonResponse)!;
_sessionRefreshedAt = DateTime.UtcNow;

Expand Down
146 changes: 116 additions & 30 deletions src/X.Bluesky/BlueskyClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Globalization;
using System.Net.Http.Headers;
using System.Security.Authentication;
Expand All @@ -8,6 +9,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using X.Bluesky.EmbedCards;
using X.Bluesky.Models;

namespace X.Bluesky;
Expand All @@ -30,11 +32,37 @@ public interface IBlueskyClient
/// <param name="text">
/// Post text
/// </param>
/// <param name="uri">
/// <param name="url">
/// Url of attachment page
/// </param>
/// <returns></returns>
Task Post(string text, Uri uri);
Task Post(string text, Uri url);

/// <summary>
/// Create post with image
/// </summary>
/// <param name="text"></param>
/// <param name="image"></param>
/// <returns></returns>
Task Post(string text, Image image);

/// <summary>
/// Create post with link and image
/// </summary>
/// <param name="text"></param>
/// <param name="url"></param>
/// <param name="image"></param>
/// <returns></returns>
Task Post(string text, Uri? url, Image image);

/// <summary>
/// Create post with link and images
/// </summary>
/// <param name="text"></param>
/// <param name="url"></param>
/// <param name="images"></param>
/// <returns></returns>
Task Post(string text, Uri? url, IEnumerable<Image> images);
}

public class BlueskyClient : IBlueskyClient
Expand All @@ -43,6 +71,7 @@ public class BlueskyClient : IBlueskyClient
private readonly IAuthorizationClient _authorizationClient;
private readonly IMentionResolver _mentionResolver;
private readonly IHttpClientFactory _httpClientFactory;
private readonly Uri _baseUrl;
private readonly IReadOnlyCollection<string> _languages;

/// <summary>
Expand All @@ -61,12 +90,60 @@ public BlueskyClient(
IEnumerable<string> languages,
bool reuseSession,
ILogger<BlueskyClient> logger)
: this(httpClientFactory, identifier, password, languages, reuseSession, new Uri("https://bsky.social"), logger)
{
}

/// <summary>
/// Creates a new instance of the Bluesky client
/// </summary>
/// <param name="httpClientFactory"></param>
/// <param name="languages">Post languages</param>
/// <param name="baseUrl">Bluesky base url</param>
/// <param name="logger"></param>
/// <param name="mentionResolver"></param>
/// <param name="authorizationClient"></param>
public BlueskyClient(
IHttpClientFactory httpClientFactory,
IEnumerable<string> languages,
Uri baseUrl,
IMentionResolver mentionResolver,
IAuthorizationClient authorizationClient,
ILogger<BlueskyClient> logger)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_baseUrl = baseUrl;
_languages = languages.ToFrozenSet();
_mentionResolver = new MentionResolver(_httpClientFactory);
_authorizationClient = new AuthorizationClient(httpClientFactory, identifier, password, reuseSession);
_mentionResolver = mentionResolver;
_authorizationClient = authorizationClient;
}

/// <summary>
/// Creates a new instance of the Bluesky client
/// </summary>
/// <param name="httpClientFactory"></param>
/// <param name="identifier">User identifier</param>
/// <param name="password">User password or application password</param>
/// <param name="languages">Post languages</param>
/// <param name="reuseSession">Indicates whether to reuse the session</param>
/// <param name="baseUrl">Bluesky base url</param>>
/// <param name="logger">Logger</param>
public BlueskyClient(
IHttpClientFactory httpClientFactory,
string identifier,
string password,
IEnumerable<string> languages,
bool reuseSession,
Uri baseUrl,
ILogger<BlueskyClient> logger)
: this(
httpClientFactory,
languages,
baseUrl,
new MentionResolver(httpClientFactory, baseUrl, logger),
new AuthorizationClient(httpClientFactory, identifier, password, reuseSession, baseUrl), logger)
{
}

/// <summary>
Expand Down Expand Up @@ -106,19 +183,19 @@ public BlueskyClient(string identifier, string password)
}

/// <inheritdoc />
public Task Post(string text) => CreatePost(text, null);
public Task Post(string text) => Post(text, null, ImmutableList<Image>.Empty);

/// <inheritdoc />
public Task Post(string text, Uri uri) => CreatePost(text, uri);
public Task Post(string text, Uri url) => Post(text, url, ImmutableList<Image>.Empty);

/// <inheritdoc />
public Task Post(string text, Image image) => Post(text, null, image);

/// <inheritdoc />
public Task Post(string text, Uri? url, Image image) => Post(text, url, ImmutableList.Create(image));

/// <summary>
/// Create post
/// </summary>
/// <param name="text">Post text</param>
/// <param name="url"></param>
/// <returns></returns>
private async Task CreatePost(string text, Uri? url)
/// <inheritdoc />
public async Task Post(string text, Uri? url, IEnumerable<Image> images)
{
var session = await _authorizationClient.GetSession();

Expand Down Expand Up @@ -156,29 +233,36 @@ private async Task CreatePost(string text, Uri? url)
Facets = facets.ToList()
};

if (url == null)
if (images.Any())
{
//If no link was defined we're trying to get link from facets
url = facets
.SelectMany(facet => facet.Features)
.Where(feature => feature is FacetFeatureLink)
.Cast<FacetFeatureLink>()
.Select(f => f.Uri)
.FirstOrDefault();
}
var embedBuilder = new EmbedImageBuilder(_httpClientFactory, session, _baseUrl, _logger);

if (url != null)
post.Embed = await embedBuilder.GetEmbedCard(images);
}
else
{
var embedCardBuilder = new EmbedCardBuilder(_httpClientFactory, session, _logger);
//If no image was defined we're trying to get link from facets

if (url == null)
{
//If no link was defined we're trying to get link from facets
url = facets
.SelectMany(facet => facet.Features)
.Where(feature => feature is FacetFeatureLink)
.Cast<FacetFeatureLink>()
.Select(f => f.Uri)
.FirstOrDefault();
}

post.Embed = new Embed
if (url != null)
{
External = await embedCardBuilder.GetEmbedCard(url),
Type = "app.bsky.embed.external"
};
var embedBuilder = new EmbedExternalBuilder(_httpClientFactory, session, _baseUrl, _logger);

post.Embed = await embedBuilder.GetEmbedCard(url);
}
}

var requestUri = "https://bsky.social/xrpc/com.atproto.repo.createRecord";
var requestUri = $"{_baseUrl.ToString().TrimEnd('/')}/xrpc/com.atproto.repo.createRecord";

var requestData = new CreatePostRequest
{
Expand Down Expand Up @@ -213,4 +297,6 @@ private async Task CreatePost(string text, Uri? url)
// This throws an exception if the HTTP response status is an error code.
response.EnsureSuccessStatusCode();
}
}


}
Loading

0 comments on commit 427233a

Please sign in to comment.