Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

27 Implement image attachment for posts #29

Merged
merged 26 commits into from
Dec 12, 2024
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
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
Loading