From 1362c8cba95de0d21093f04e375d392abd660760 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 11:25:40 +0300 Subject: [PATCH 01/15] Add FacetBuilder --- src/X.Bluesky/BlueskyClient.cs | 61 +++++++- src/X.Bluesky/FacetBuilder.cs | 130 ++++++++++++++++++ src/X.Bluesky/Models/Facet.cs | 28 +++- .../X.Bluesky.Tests/BlueskyIntegrationTest.cs | 3 +- tests/X.Bluesky.Tests/PostCreatingTest.cs | 20 +++ 5 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 src/X.Bluesky/FacetBuilder.cs create mode 100644 tests/X.Bluesky.Tests/PostCreatingTest.cs diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index c0678b3..c36153f 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -2,6 +2,7 @@ using System.Net.Http.Headers; using System.Security.Authentication; using System.Text; +using System.Text.RegularExpressions; using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -96,13 +97,17 @@ private async Task CreatePost(string text, Uri? url) // Fetch the current time in ISO 8601 format, with "Z" to denote UTC var now = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); + + var facets = await GetFacets(text); + // Required fields for the post var post = new Post { Type = "app.bsky.feed.post", Text = text, CreatedAt = now, - Langs = _languages.ToList() + Langs = _languages.ToList(), + Facets = facets }; if (url != null) @@ -122,7 +127,7 @@ private async Task CreatePost(string text, Uri? url) { Repo = session.Did, Collection = "app.bsky.feed.post", - Record = post + Record = post, }; var jsonRequest = JsonConvert.SerializeObject(requestData, Formatting.Indented, new JsonSerializerSettings @@ -144,7 +149,57 @@ private async Task CreatePost(string text, Uri? url) // This throws an exception if the HTTP response status is an error code. response.EnsureSuccessStatusCode(); } - + + public async Task> GetFacets(string text) + { + const string pattern = "rb\"[$|\\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\""; + + var matches = Regex.Matches(text, pattern); + + var facets = new List(); + var facetBuilder = new FacetBuilder(); + + foreach (Match match in matches) + { + if (match.Success) + { + var facetFeature = facetBuilder.Create(match.Value); + + if (facetFeature != null) + { + + + facets.Add(new Facet + { + Index = new FacetIndex + { + ByteStart = 1, + ByteEnd = 1 + }, + Features = [facetFeature] + }); + } + } + } + // { + // text: 'Go to this site', + // facets: [ + // { + // index: { + // byteStart: 6, + // byteEnd: 15 + // }, + // features: [{ + // $type: 'app.bsky.richtext.facet#link', + // uri: 'https://example.com' + // }] + // } + // ] + // } + + return facets; + } + /// /// Create post /// diff --git a/src/X.Bluesky/FacetBuilder.cs b/src/X.Bluesky/FacetBuilder.cs new file mode 100644 index 0000000..a1b71b9 --- /dev/null +++ b/src/X.Bluesky/FacetBuilder.cs @@ -0,0 +1,130 @@ +using X.Bluesky.Models; + +namespace X.Bluesky; + +public class FacetBuilder +{ + public FacetFeature? Create(string text) + { + FacetFeature? facetFeature = null; + + if (IsFacetFeatureLink(text)) + { + facetFeature = CreateFacetFeatureLink(); + } + else if (IsFacetFeatureMention(text)) + { + facetFeature = CreateFacetFeatureMention(); + } + else if (IsFacetFeatureTag(text)) + { + facetFeature = CreateFacetFeatureTag(); + } + + return facetFeature; + } + + private bool IsFacetFeatureTag(string text) + { + throw new NotImplementedException(); + } + + private bool IsFacetFeatureMention(string text) + { + throw new NotImplementedException(); + } + + private bool IsFacetFeatureLink(string text) + { + throw new NotImplementedException(); + } + + private static FacetFeatureTag CreateFacetFeatureTag() + { + return new FacetFeatureTag(); + } + + private static FacetFeatureMention CreateFacetFeatureMention() + { + return new FacetFeatureMention(); + } + + private static FacetFeatureLink CreateFacetFeatureLink() + { + return new FacetFeatureLink(); + } + + // public static List DetectFacets(string text) + // { + // var facets = new List(); + // + // // Detect links (http/https URLs) + // var linkRegex = new Regex(@"https?:\/\/[^\s]+"); + // foreach (Match match in linkRegex.Matches(text)) + // { + // facets.Add(new Facet + // { + // Index = new Index + // { + // ByteStart = match.Index, + // ByteEnd = match.Index + match.Length + // }, + // Features = new List + // { + // new Feature + // { + // Type = "app.bsky.richtext.facet#link", + // Uri = match.Value + // } + // } + // }); + // } + // + // // Detect hashtags (#tag) + // var tagRegex = new Regex(@"#\w+"); + // foreach (Match match in tagRegex.Matches(text)) + // { + // facets.Add(new Facet + // { + // Index = new Index + // { + // ByteStart = match.Index, + // ByteEnd = match.Index + match.Length + // }, + // Features = new List + // { + // new Feature + // { + // Type = "app.bsky.richtext.facet#tag", + // Tag = match.Value + // } + // } + // }); + // } + // + // // Detect mentions (@username) + // var mentionRegex = new Regex(@"@\w+"); + // foreach (Match match in mentionRegex.Matches(text)) + // { + // facets.Add(new Facet + // { + // Index = new Index + // { + // ByteStart = match.Index, + // ByteEnd = match.Index + match.Length + // }, + // Features = new List + // { + // new Feature + // { + // Type = "app.bsky.richtext.facet#mention", + // Did = match.Value + // } + // } + // }); + // } + // + // return facets; + // } +} + diff --git a/src/X.Bluesky/Models/Facet.cs b/src/X.Bluesky/Models/Facet.cs index 52f52e5..37d12ac 100644 --- a/src/X.Bluesky/Models/Facet.cs +++ b/src/X.Bluesky/Models/Facet.cs @@ -9,10 +9,32 @@ public record Facet public List Features { get; set; } = new(); } -public record FacetFeature +public abstract record FacetFeature { [JsonProperty("$type")] - public string Type { get; set; } = ""; + public abstract string Type { get; } +} + +public record FacetFeatureLink : FacetFeature +{ + public Uri Uri { get; set; } + public override string Type => "app.bsky.richtext.facet#link"; +} + +public record FacetFeatureMention : FacetFeature +{ + //did: match[3], // must be resolved afterwards + + public string Did { get; set; } + + public override string Type => "app.bsky.richtext.facet#mention"; +} + +public record FacetFeatureTag : FacetFeature +{ + //tag: tag.replace(/^#/, ''), + + public string Tag { get; set; } - public Uri? Uri { get; set; } + public override string Type => "app.bsky.richtext.facet#tag"; } \ No newline at end of file diff --git a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs index 73326b0..e16edd7 100644 --- a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs +++ b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs @@ -1,9 +1,8 @@ using System; using System.Threading.Tasks; -using X.Bluesky; using Xunit; -namespace Tests; +namespace X.Bluesky.Tests; public class BlueskyIntegrationTest { diff --git a/tests/X.Bluesky.Tests/PostCreatingTest.cs b/tests/X.Bluesky.Tests/PostCreatingTest.cs new file mode 100644 index 0000000..fc45559 --- /dev/null +++ b/tests/X.Bluesky.Tests/PostCreatingTest.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace X.Bluesky.Tests; + +public class PostCreatingTest +{ + [Fact] + public async Task CheckFacets() + { + + var client = new BlueskyClient("identifier", "password"); + + var text = "This is a test and this is a #tag and https://example.com link"; + var facets = await client.GetFacets(text); + + Assert.Equal(2, facets.Count); + } +} \ No newline at end of file From 8db2383e7864850fdc3e8eadbff09413f2b9a506 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 11:36:41 +0300 Subject: [PATCH 02/15] Update FacetBuilder --- src/X.Bluesky/FacetBuilder.cs | 181 +++++++++++++++------------------- 1 file changed, 79 insertions(+), 102 deletions(-) diff --git a/src/X.Bluesky/FacetBuilder.cs b/src/X.Bluesky/FacetBuilder.cs index a1b71b9..f02a640 100644 --- a/src/X.Bluesky/FacetBuilder.cs +++ b/src/X.Bluesky/FacetBuilder.cs @@ -1,130 +1,107 @@ +using System.Text.RegularExpressions; using X.Bluesky.Models; namespace X.Bluesky; public class FacetBuilder { - public FacetFeature? Create(string text) + public IReadOnlyCollection Create(string text) { - FacetFeature? facetFeature = null; + var result = new List(); + // FacetFeature? facetFeature = null; - if (IsFacetFeatureLink(text)) + var featureLinkMatches = GetFeatureLinkMatches(text); + var featureMentionMatches = GetFeatureMentionMatches(text); + var featureTagMatches = GetFeatureTagMatches(text); + + foreach (var match in featureLinkMatches) { - facetFeature = CreateFacetFeatureLink(); + result.Add(new Facet + { + Index = new FacetIndex + { + ByteStart = match.Index, + ByteEnd = match.Index + match.Length + }, + Features = + [ + new FacetFeatureLink { Uri = new Uri(match.Value) } + ] + }); } - else if (IsFacetFeatureMention(text)) + + foreach (var match in featureMentionMatches) { - facetFeature = CreateFacetFeatureMention(); + result.Add(new Facet + { + Index = new FacetIndex + { + ByteStart = match.Index, + ByteEnd = match.Index + match.Length + }, + Features = + [ + new FacetFeatureMention { Did = match.Value } + ] + }); } - else if (IsFacetFeatureTag(text)) + + foreach (var match in featureTagMatches) { - facetFeature = CreateFacetFeatureTag(); + result.Add(new Facet + { + Index = new FacetIndex + { + ByteStart = match.Index, + ByteEnd = match.Index + match.Length + }, + Features = + [ + new FacetFeatureTag { Tag = match.Value } + ] + }); } - return facetFeature; + return result; } - private bool IsFacetFeatureTag(string text) + /// + /// Detect hashtags + /// + /// + /// + private IReadOnlyCollection GetFeatureTagMatches(string text) { - throw new NotImplementedException(); - } + var regex = new Regex(@"#\w+"); + var matches = regex.Matches(text).ToList(); - private bool IsFacetFeatureMention(string text) - { - throw new NotImplementedException(); - } + return matches; - private bool IsFacetFeatureLink(string text) - { - throw new NotImplementedException(); } - private static FacetFeatureTag CreateFacetFeatureTag() + /// + /// Detect mentions + /// + /// + /// + private IReadOnlyCollection GetFeatureMentionMatches(string text) { - return new FacetFeatureTag(); - } + var regex = new Regex(@"@\w+"); + var matches = regex.Matches(text).ToList(); - private static FacetFeatureMention CreateFacetFeatureMention() - { - return new FacetFeatureMention(); + return matches; } - private static FacetFeatureLink CreateFacetFeatureLink() + /// + /// Detect tags + /// + /// + /// + private IReadOnlyCollection GetFeatureLinkMatches(string text) { - return new FacetFeatureLink(); - } - - // public static List DetectFacets(string text) - // { - // var facets = new List(); - // - // // Detect links (http/https URLs) - // var linkRegex = new Regex(@"https?:\/\/[^\s]+"); - // foreach (Match match in linkRegex.Matches(text)) - // { - // facets.Add(new Facet - // { - // Index = new Index - // { - // ByteStart = match.Index, - // ByteEnd = match.Index + match.Length - // }, - // Features = new List - // { - // new Feature - // { - // Type = "app.bsky.richtext.facet#link", - // Uri = match.Value - // } - // } - // }); - // } - // - // // Detect hashtags (#tag) - // var tagRegex = new Regex(@"#\w+"); - // foreach (Match match in tagRegex.Matches(text)) - // { - // facets.Add(new Facet - // { - // Index = new Index - // { - // ByteStart = match.Index, - // ByteEnd = match.Index + match.Length - // }, - // Features = new List - // { - // new Feature - // { - // Type = "app.bsky.richtext.facet#tag", - // Tag = match.Value - // } - // } - // }); - // } - // - // // Detect mentions (@username) - // var mentionRegex = new Regex(@"@\w+"); - // foreach (Match match in mentionRegex.Matches(text)) - // { - // facets.Add(new Facet - // { - // Index = new Index - // { - // ByteStart = match.Index, - // ByteEnd = match.Index + match.Length - // }, - // Features = new List - // { - // new Feature - // { - // Type = "app.bsky.richtext.facet#mention", - // Did = match.Value - // } - // } - // }); - // } - // - // return facets; - // } -} + var regex = new Regex(@"https?:\/\/[^\s]+"); + var matches = regex.Matches(text).ToList(); + return matches; + } +} \ No newline at end of file From 78bc34eac6fb1b2dadd90ede8cba8fdaa4c651eb Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 11:38:06 +0300 Subject: [PATCH 03/15] Update FacetBuilder --- src/X.Bluesky/BlueskyClient.cs | 55 ++--------------------- tests/X.Bluesky.Tests/PostCreatingTest.cs | 6 +-- 2 files changed, 6 insertions(+), 55 deletions(-) diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index c36153f..8bce982 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -96,9 +96,9 @@ private async Task CreatePost(string text, Uri? url) // Fetch the current time in ISO 8601 format, with "Z" to denote UTC var now = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); + var facetBuilder = new FacetBuilder(); - - var facets = await GetFacets(text); + var facets = facetBuilder.Create(text).ToList(); // Required fields for the post var post = new Post @@ -107,7 +107,7 @@ private async Task CreatePost(string text, Uri? url) Text = text, CreatedAt = now, Langs = _languages.ToList(), - Facets = facets + Facets = facets }; if (url != null) @@ -150,55 +150,6 @@ private async Task CreatePost(string text, Uri? url) response.EnsureSuccessStatusCode(); } - public async Task> GetFacets(string text) - { - const string pattern = "rb\"[$|\\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\""; - - var matches = Regex.Matches(text, pattern); - - var facets = new List(); - var facetBuilder = new FacetBuilder(); - - foreach (Match match in matches) - { - if (match.Success) - { - var facetFeature = facetBuilder.Create(match.Value); - - if (facetFeature != null) - { - - - facets.Add(new Facet - { - Index = new FacetIndex - { - ByteStart = 1, - ByteEnd = 1 - }, - Features = [facetFeature] - }); - } - } - } - // { - // text: 'Go to this site', - // facets: [ - // { - // index: { - // byteStart: 6, - // byteEnd: 15 - // }, - // features: [{ - // $type: 'app.bsky.richtext.facet#link', - // uri: 'https://example.com' - // }] - // } - // ] - // } - - return facets; - } /// /// Create post diff --git a/tests/X.Bluesky.Tests/PostCreatingTest.cs b/tests/X.Bluesky.Tests/PostCreatingTest.cs index fc45559..999a92a 100644 --- a/tests/X.Bluesky.Tests/PostCreatingTest.cs +++ b/tests/X.Bluesky.Tests/PostCreatingTest.cs @@ -9,11 +9,11 @@ public class PostCreatingTest [Fact] public async Task CheckFacets() { - - var client = new BlueskyClient("identifier", "password"); + var facetBuilder = new FacetBuilder(); + var text = "This is a test and this is a #tag and https://example.com link"; - var facets = await client.GetFacets(text); + var facets = facetBuilder.Create(text); Assert.Equal(2, facets.Count); } From 7b896d9ab34d6cb6ed5b38e3e6fa427dfd8b3a39 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 11:44:03 +0300 Subject: [PATCH 04/15] Update --- src/X.Bluesky/FacetBuilder.cs | 68 ++++++++++------------- tests/X.Bluesky.Tests/PostCreatingTest.cs | 18 +++++- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/X.Bluesky/FacetBuilder.cs b/src/X.Bluesky/FacetBuilder.cs index f02a640..24d6290 100644 --- a/src/X.Bluesky/FacetBuilder.cs +++ b/src/X.Bluesky/FacetBuilder.cs @@ -8,7 +8,6 @@ public class FacetBuilder public IReadOnlyCollection Create(string text) { var result = new List(); - // FacetFeature? facetFeature = null; var featureLinkMatches = GetFeatureLinkMatches(text); var featureMentionMatches = GetFeatureMentionMatches(text); @@ -16,55 +15,49 @@ public IReadOnlyCollection Create(string text) foreach (var match in featureLinkMatches) { - result.Add(new Facet - { - Index = new FacetIndex - { - ByteStart = match.Index, - ByteEnd = match.Index + match.Length - }, - Features = - [ - new FacetFeatureLink { Uri = new Uri(match.Value) } - ] - }); + var start = match.Index; + var end = start + match.Length; + + result.Add(CreateFacet(start, end, new FacetFeatureLink { Uri = new Uri(match.Value) })); } foreach (var match in featureMentionMatches) { - result.Add(new Facet - { - Index = new FacetIndex - { - ByteStart = match.Index, - ByteEnd = match.Index + match.Length - }, - Features = - [ - new FacetFeatureMention { Did = match.Value } - ] - }); + var start = match.Index; + var end = start + match.Length; + + result.Add(CreateFacet(start, end, new FacetFeatureMention { Did = match.Value })); } foreach (var match in featureTagMatches) { - result.Add(new Facet - { - Index = new FacetIndex - { - ByteStart = match.Index, - ByteEnd = match.Index + match.Length - }, - Features = - [ - new FacetFeatureTag { Tag = match.Value } - ] - }); + var start = match.Index; + var end = start + match.Length; + + result.Add(CreateFacet(start, end, new FacetFeatureTag { Tag = match.Value })); } return result; } + private Facet CreateFacet(int start, int end, FacetFeature facetFeature) + { + var result = new Facet + { + Index = new FacetIndex + { + ByteStart = start, + ByteEnd = end + }, + Features = + [ + facetFeature + ] + }; + + return result; + } + /// /// Detect hashtags /// @@ -76,7 +69,6 @@ private IReadOnlyCollection GetFeatureTagMatches(string text) var matches = regex.Matches(text).ToList(); return matches; - } /// diff --git a/tests/X.Bluesky.Tests/PostCreatingTest.cs b/tests/X.Bluesky.Tests/PostCreatingTest.cs index 999a92a..e7fd189 100644 --- a/tests/X.Bluesky.Tests/PostCreatingTest.cs +++ b/tests/X.Bluesky.Tests/PostCreatingTest.cs @@ -7,14 +7,26 @@ namespace X.Bluesky.Tests; public class PostCreatingTest { [Fact] - public async Task CheckFacets() + public async Task CheckFacets_Exist() { var facetBuilder = new FacetBuilder(); + var text = "This is a test and this is a #tag and https://example.com link and metions @one and @two in the end of text"; + + var facets = facetBuilder.Create(text); + + Assert.Equal(4, facets.Count); + } + + [Fact] + public async Task CheckFacets_Empty() + { + var facetBuilder = new FacetBuilder(); - var text = "This is a test and this is a #tag and https://example.com link"; + var text = "This is a test and this is a no link and no metions in the end of text"; + var facets = facetBuilder.Create(text); - Assert.Equal(2, facets.Count); + Assert.Empty(facets); } } \ No newline at end of file From 8dcfd537198bd10ec154bb9b482a02d02ec7b374 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 12:15:59 +0300 Subject: [PATCH 05/15] Add test --- src/X.Bluesky/BlueskyClient.cs | 4 ++-- src/X.Bluesky/FacetBuilder.cs | 16 ++++++++++------ tests/X.Bluesky.Tests/PostCreatingTest.cs | 19 +++++++++++++++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index 8bce982..0a0885d 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -98,7 +98,7 @@ private async Task CreatePost(string text, Uri? url) var now = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); var facetBuilder = new FacetBuilder(); - var facets = facetBuilder.Create(text).ToList(); + var facets = facetBuilder.Create(text); // Required fields for the post var post = new Post @@ -107,7 +107,7 @@ private async Task CreatePost(string text, Uri? url) Text = text, CreatedAt = now, Langs = _languages.ToList(), - Facets = facets + Facets = facets.ToList() }; if (url != null) diff --git a/src/X.Bluesky/FacetBuilder.cs b/src/X.Bluesky/FacetBuilder.cs index 24d6290..98bd4b0 100644 --- a/src/X.Bluesky/FacetBuilder.cs +++ b/src/X.Bluesky/FacetBuilder.cs @@ -3,6 +3,9 @@ namespace X.Bluesky; +/// +/// +/// public class FacetBuilder { public IReadOnlyCollection Create(string text) @@ -33,14 +36,15 @@ public IReadOnlyCollection Create(string text) { var start = match.Index; var end = start + match.Length; - - result.Add(CreateFacet(start, end, new FacetFeatureTag { Tag = match.Value })); + var tag = match.Value.Replace("#", string.Empty); + + result.Add(CreateFacet(start, end, new FacetFeatureTag { Tag = tag })); } return result; } - private Facet CreateFacet(int start, int end, FacetFeature facetFeature) + public Facet CreateFacet(int start, int end, FacetFeature facetFeature) { var result = new Facet { @@ -63,7 +67,7 @@ private Facet CreateFacet(int start, int end, FacetFeature facetFeature) /// /// /// - private IReadOnlyCollection GetFeatureTagMatches(string text) + public IReadOnlyCollection GetFeatureTagMatches(string text) { var regex = new Regex(@"#\w+"); var matches = regex.Matches(text).ToList(); @@ -76,7 +80,7 @@ private IReadOnlyCollection GetFeatureTagMatches(string text) /// /// /// - private IReadOnlyCollection GetFeatureMentionMatches(string text) + public IReadOnlyCollection GetFeatureMentionMatches(string text) { var regex = new Regex(@"@\w+"); var matches = regex.Matches(text).ToList(); @@ -89,7 +93,7 @@ private IReadOnlyCollection GetFeatureMentionMatches(string text) /// /// /// - private IReadOnlyCollection GetFeatureLinkMatches(string text) + public IReadOnlyCollection GetFeatureLinkMatches(string text) { var regex = new Regex(@"https?:\/\/[^\s]+"); var matches = regex.Matches(text).ToList(); diff --git a/tests/X.Bluesky.Tests/PostCreatingTest.cs b/tests/X.Bluesky.Tests/PostCreatingTest.cs index e7fd189..036bd8c 100644 --- a/tests/X.Bluesky.Tests/PostCreatingTest.cs +++ b/tests/X.Bluesky.Tests/PostCreatingTest.cs @@ -1,4 +1,4 @@ -using System; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -11,13 +11,28 @@ public async Task CheckFacets_Exist() { var facetBuilder = new FacetBuilder(); - var text = "This is a test and this is a #tag and https://example.com link and metions @one and @two in the end of text"; + //var text = "This is a test and this is a #tag and https://example.com link and metions @one and @two in the end of text"; + var text = $"Hello world! This post contains #devdigest and #microsoft also it include https://devdigest.today which is in middle of post text and @andrew.gubskiy.com mention"; var facets = facetBuilder.Create(text); Assert.Equal(4, facets.Count); } + [Fact] + public async Task CheckFacetsMetion() + { + var facetBuilder = new FacetBuilder(); + + var text = "Hello world! This post contains #devdigest and #microsoft also it include https://devdigest.today which is in middle of post text and @andrew.gubskiy.com mention"; + + var matches = facetBuilder.GetFeatureMentionMatches(text); + var match = matches.ToList().FirstOrDefault(); + + + Assert.Equal("@andrew.gubskiy.com", match.Value); + } + [Fact] public async Task CheckFacets_Empty() { From 9522b0dc63256eb62b944cd14da80f07894ed3bd Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 12:56:22 +0300 Subject: [PATCH 06/15] Update mention resolving --- src/X.Bluesky/BlueskyClient.cs | 24 ++++++- src/X.Bluesky/FacetBuilder.cs | 3 +- src/X.Bluesky/MentionResolver.cs | 59 +++++++++++++++++ src/X.Bluesky/Models/Facet.cs | 32 ---------- src/X.Bluesky/Models/FacetFeature.cs | 64 +++++++++++++++++++ .../X.Bluesky.Tests/BlueskyIntegrationTest.cs | 18 +++++- 6 files changed, 164 insertions(+), 36 deletions(-) create mode 100644 src/X.Bluesky/MentionResolver.cs create mode 100644 src/X.Bluesky/Models/FacetFeature.cs diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index 0a0885d..7537a9f 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using X.Bluesky.Models; @@ -36,6 +37,7 @@ public class BlueskyClient : IBlueskyClient private readonly string _identifier; private readonly string _password; private readonly ILogger _logger; + private readonly IMentionResolver _mentionResolver; private readonly IHttpClientFactory _httpClientFactory; private readonly IReadOnlyCollection _languages; @@ -59,6 +61,7 @@ public BlueskyClient( _password = password; _logger = logger; _languages = languages.ToImmutableList(); + _mentionResolver = new MentionResolver(_httpClientFactory); } /// @@ -100,6 +103,19 @@ private async Task CreatePost(string text, Uri? url) var facets = facetBuilder.Create(text); + foreach (var facet in facets) + { + foreach (var facetFeature in facet.Features) + { + if (facetFeature is FacetFeatureMention facetFeatureMention) + { + var resolveDid = await _mentionResolver.ResolveMention(facetFeatureMention.Did); + + facetFeatureMention.ResolveDid(resolveDid); + } + } + } + // Required fields for the post var post = new Post { @@ -146,11 +162,17 @@ private async Task CreatePost(string text, Uri? url) var response = await httpClient.PostAsync(requestUri, content); + if (!response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + + _logger.LogError(responseContent); + } + // This throws an exception if the HTTP response status is an error code. response.EnsureSuccessStatusCode(); } - /// /// Create post /// diff --git a/src/X.Bluesky/FacetBuilder.cs b/src/X.Bluesky/FacetBuilder.cs index 98bd4b0..ba4919b 100644 --- a/src/X.Bluesky/FacetBuilder.cs +++ b/src/X.Bluesky/FacetBuilder.cs @@ -82,7 +82,8 @@ public IReadOnlyCollection GetFeatureTagMatches(string text) /// public IReadOnlyCollection GetFeatureMentionMatches(string text) { - var regex = new Regex(@"@\w+"); + // var regex = new Regex(@"@\w+"); + var regex = new Regex(@"@\w+(\.\w+)*"); var matches = regex.Matches(text).ToList(); return matches; diff --git a/src/X.Bluesky/MentionResolver.cs b/src/X.Bluesky/MentionResolver.cs new file mode 100644 index 0000000..5cfcf5f --- /dev/null +++ b/src/X.Bluesky/MentionResolver.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json.Linq; + +namespace X.Bluesky; + +public interface IMentionResolver +{ + Task ResolveMention(string mention); +} + +public class MentionResolver : IMentionResolver +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public MentionResolver(IHttpClientFactory httpClientFactory, ILogger logger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + public MentionResolver(IHttpClientFactory httpClientFactory, ILogger logger) + : this(httpClientFactory, (ILogger)logger) + { + } + + public MentionResolver(IHttpClientFactory httpClientFactory) + : this(httpClientFactory, new NullLogger()) + { + } + + public async Task ResolveMention(string mention) + { + var httpClient = _httpClientFactory.CreateClient(); + + const string url = "https://bsky.social/xrpc/com.atproto.identity.resolveHandle"; + + var requestUri = $"{url}?handle={mention.Replace("@", string.Empty)}"; + + var response = await httpClient.GetAsync(requestUri); + + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var json = JObject.Parse(responseContent); + + return json["did"]?.ToString() ?? string.Empty; + } + else + { + _logger.LogError(responseContent); + } + + // Return null if unable to resolve + return string.Empty; + } +} \ No newline at end of file diff --git a/src/X.Bluesky/Models/Facet.cs b/src/X.Bluesky/Models/Facet.cs index 37d12ac..2c5cbb4 100644 --- a/src/X.Bluesky/Models/Facet.cs +++ b/src/X.Bluesky/Models/Facet.cs @@ -1,5 +1,3 @@ -using Newtonsoft.Json; - namespace X.Bluesky.Models; public record Facet @@ -8,33 +6,3 @@ public record Facet public List Features { get; set; } = new(); } - -public abstract record FacetFeature -{ - [JsonProperty("$type")] - public abstract string Type { get; } -} - -public record FacetFeatureLink : FacetFeature -{ - public Uri Uri { get; set; } - public override string Type => "app.bsky.richtext.facet#link"; -} - -public record FacetFeatureMention : FacetFeature -{ - //did: match[3], // must be resolved afterwards - - public string Did { get; set; } - - public override string Type => "app.bsky.richtext.facet#mention"; -} - -public record FacetFeatureTag : FacetFeature -{ - //tag: tag.replace(/^#/, ''), - - public string Tag { get; set; } - - public override string Type => "app.bsky.richtext.facet#tag"; -} \ No newline at end of file diff --git a/src/X.Bluesky/Models/FacetFeature.cs b/src/X.Bluesky/Models/FacetFeature.cs new file mode 100644 index 0000000..01d5162 --- /dev/null +++ b/src/X.Bluesky/Models/FacetFeature.cs @@ -0,0 +1,64 @@ +using Newtonsoft.Json; + +namespace X.Bluesky.Models; + +public abstract record FacetFeature +{ + [JsonProperty("$type")] + public abstract string Type { get; } +} + +public record FacetFeatureLink : FacetFeature +{ + public Uri Uri { get; set; } + public override string Type => "app.bsky.richtext.facet#link"; +} + +public record FacetFeatureMention : FacetFeature +{ + /// + /// Important! Did must be resolved from @username to did:plc value + /// + public string Did { get; set; } = ""; + + /// + /// + /// + public bool IsResolved => IsCorrectDid(Did); + + public void ResolveDid(string value) + { + if (!IsCorrectDid(value)) + { + throw new FormatException("Did not recognize"); + } + + Did = value; + } + + private bool IsCorrectDid(string did) + { + if (string.IsNullOrWhiteSpace(did)) + { + return false; + } + + if (did.Contains("did:plc:")) + { + return true; + } + + return false; + } + + public override string Type => "app.bsky.richtext.facet#mention"; +} + +public record FacetFeatureTag : FacetFeature +{ + //tag: tag.replace(/^#/, ''), + + public string Tag { get; set; } + + public override string Type => "app.bsky.richtext.facet#tag"; +} \ No newline at end of file diff --git a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs index e16edd7..e7c3669 100644 --- a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs +++ b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs @@ -15,9 +15,23 @@ public async Task CheckSending() IBlueskyClient client = new BlueskyClient(identifier, password); var link = new Uri("https://devdigest.today/post/2431"); - - await client.Post("Hello world!", link); + var text = $"Hello world! This post contains #devdigest and #microsoft also it include {link} which is in middle of post text and @andrew.gubskiy.com mention"; + + await client.Post(text, link); Assert.True(true); } + + [Fact] + public async Task TestResolveMention() + { + var mention = "@andrew.gubskiy.com"; + var httpClientFactory = new HttpClientFactory(); + + IMentionResolver mentionResolver = new MentionResolver(httpClientFactory); + + var did = await mentionResolver.ResolveMention(mention); + + Assert.NotEmpty(did); + } } \ No newline at end of file From 40fd95282eeedeff1b71718cc16870b97db59116 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 13:06:46 +0300 Subject: [PATCH 07/15] Update project parameters --- README.md | 7 ++++--- X.Bluesky.sln | 3 +++ src/Directory.Build.props | 24 +++++++++++++++++++++++ src/X.Bluesky/BlueskyClient.cs | 4 +--- src/X.Bluesky/FacetBuilder.cs | 7 ++++++- src/X.Bluesky/MentionResolver.cs | 3 +++ src/X.Bluesky/X.Bluesky.csproj | 18 +++-------------- tests/X.Bluesky.Tests/PostCreatingTest.cs | 4 ++-- 8 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 src/Directory.Build.props diff --git a/README.md b/README.md index f713c1d..2aff13a 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ By leveraging the Bluesky API, this project allows for straightforward integrati ## Features -- Post messages directly to Bluesky. -- Attach links to posts, allowing for page previews within the Bluesky feed. -- Authenticate with Bluesky using an identifier and password. +- Post messages directly to Bluesky +- Attach links to posts, allowing for page previews within the Bluesky feed +- Authenticate with Bluesky using an identifier and password +- Automatically generate tags, mentions and url cards ## Getting Started diff --git a/X.Bluesky.sln b/X.Bluesky.sln index 1a26d5f..079057e 100644 --- a/X.Bluesky.sln +++ b/X.Bluesky.sln @@ -4,6 +4,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 15.0.27004.2008 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5B9BA258-2A81-4F93-808A-80F0823F7EDB}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4E157A7F-77D1-43D9-B7D2-88E846AB091F}" EndProject diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..5a90161 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,24 @@ + + + + True + + README.md + LICENSE.md + x-web.png + + Andrew Gubskiy + Andrew Gubskiy © 2024 + Ukrainian .NET Developer Community + + 1.1.0 + 1.1.0 + 1.1.0 + 1.1.0 + + git + https://github.com/ernado-x/X.Bluesky.git + https://andrew.gubskiy.com/open-source + + + diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index 7537a9f..ecc67e0 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -2,12 +2,10 @@ using System.Net.Http.Headers; using System.Security.Authentication; using System.Text; -using System.Text.RegularExpressions; using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using X.Bluesky.Models; @@ -101,7 +99,7 @@ private async Task CreatePost(string text, Uri? url) var now = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); var facetBuilder = new FacetBuilder(); - var facets = facetBuilder.Create(text); + var facets = facetBuilder.GetFacets(text); foreach (var facet in facets) { diff --git a/src/X.Bluesky/FacetBuilder.cs b/src/X.Bluesky/FacetBuilder.cs index ba4919b..f52d94d 100644 --- a/src/X.Bluesky/FacetBuilder.cs +++ b/src/X.Bluesky/FacetBuilder.cs @@ -8,7 +8,12 @@ namespace X.Bluesky; /// public class FacetBuilder { - public IReadOnlyCollection Create(string text) + /// + /// Get facets from the text + /// + /// + /// + public IReadOnlyCollection GetFacets(string text) { var result = new List(); diff --git a/src/X.Bluesky/MentionResolver.cs b/src/X.Bluesky/MentionResolver.cs index 5cfcf5f..536eaaf 100644 --- a/src/X.Bluesky/MentionResolver.cs +++ b/src/X.Bluesky/MentionResolver.cs @@ -9,6 +9,9 @@ public interface IMentionResolver Task ResolveMention(string mention); } +/// +/// Resolve mention to DID format +/// public class MentionResolver : IMentionResolver { private readonly IHttpClientFactory _httpClientFactory; diff --git a/src/X.Bluesky/X.Bluesky.csproj b/src/X.Bluesky/X.Bluesky.csproj index ef351c0..beb7729 100644 --- a/src/X.Bluesky/X.Bluesky.csproj +++ b/src/X.Bluesky/X.Bluesky.csproj @@ -1,25 +1,13 @@  - README.md + X.Bluesky + Simple client for posting to Bluesky enable enable - Andrew Gubskiy - Simple client for posting to Bluesky - Andrew Gubskiy - https://github.com/ernado-x/X.Bluesky - bluesky, social networks - 1.0.3.0 - 1.0.3.0 + bluesky, social networks net8.0;net6.0;netstandard2.1 default - true - X.Bluesky - https://raw.githubusercontent.com/ernado-x/X.Bluesky/main/LICENSE - icon.png - https://github.com/ernado-x/X.Bluesky - git - 1.0.3 diff --git a/tests/X.Bluesky.Tests/PostCreatingTest.cs b/tests/X.Bluesky.Tests/PostCreatingTest.cs index 036bd8c..67276bc 100644 --- a/tests/X.Bluesky.Tests/PostCreatingTest.cs +++ b/tests/X.Bluesky.Tests/PostCreatingTest.cs @@ -14,7 +14,7 @@ public async Task CheckFacets_Exist() //var text = "This is a test and this is a #tag and https://example.com link and metions @one and @two in the end of text"; var text = $"Hello world! This post contains #devdigest and #microsoft also it include https://devdigest.today which is in middle of post text and @andrew.gubskiy.com mention"; - var facets = facetBuilder.Create(text); + var facets = facetBuilder.GetFacets(text); Assert.Equal(4, facets.Count); } @@ -40,7 +40,7 @@ public async Task CheckFacets_Empty() var text = "This is a test and this is a no link and no metions in the end of text"; - var facets = facetBuilder.Create(text); + var facets = facetBuilder.GetFacets(text); Assert.Empty(facets); } From 516fa387f8805dd40c77c737e8859b9e85dca8f6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 13:24:29 +0300 Subject: [PATCH 08/15] Update test. --- src/X.Bluesky/BlueskyClient.cs | 16 ++++++++++++++-- src/X.Bluesky/EmbedCardBuilder.cs | 16 +++++++++------- tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs | 4 ++-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index ecc67e0..27674b0 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -124,13 +124,25 @@ private async Task CreatePost(string text, Uri? url) Facets = facets.ToList() }; + if (url == null) + { + //If no link was defined we're trying to get link from facets + var facetFeatureLink = facets + .SelectMany(facet => facet.Features) + .Where(feature => feature is FacetFeatureLink) + .Cast() + .FirstOrDefault(); + + url = facetFeatureLink?.Uri; + } + if (url != null) { - var embedCardBuilder = new EmbedCardBuilder(_httpClientFactory, _logger); + var embedCardBuilder = new EmbedCardBuilder(_httpClientFactory, session.AccessJwt, _logger); post.Embed = new Embed { - External = await embedCardBuilder.Create(url, session.AccessJwt), + External = await embedCardBuilder.Create(url), Type = "app.bsky.embed.external" }; } diff --git a/src/X.Bluesky/EmbedCardBuilder.cs b/src/X.Bluesky/EmbedCardBuilder.cs index bae2998..4572472 100644 --- a/src/X.Bluesky/EmbedCardBuilder.cs +++ b/src/X.Bluesky/EmbedCardBuilder.cs @@ -10,11 +10,13 @@ public class EmbedCardBuilder private readonly ILogger _logger; private readonly FileTypeHelper _fileTypeHelper; private readonly IHttpClientFactory _httpClientFactory; - - public EmbedCardBuilder(IHttpClientFactory httpClientFactory, ILogger logger) + private readonly string _accessToken; + + public EmbedCardBuilder(IHttpClientFactory httpClientFactory, string accessToken, ILogger logger) { _logger = logger; _httpClientFactory = httpClientFactory; + _accessToken = accessToken; _fileTypeHelper = new FileTypeHelper(logger); } @@ -24,7 +26,7 @@ public EmbedCardBuilder(IHttpClientFactory httpClientFactory, ILogger logger) /// /// /// - public async Task Create(Uri url, string accessToken) + public async Task Create(Uri url) { var extractor = new Web.MetaExtractor.Extractor(); var metadata = await extractor.ExtractAsync(url); @@ -44,11 +46,11 @@ public async Task Create(Uri url, string accessToken) { if (!imgUrl.Contains("://")) { - card.Thumb = await UploadImageAndSetThumbAsync(new Uri(url, imgUrl), accessToken); + card.Thumb = await UploadImageAndSetThumbAsync(new Uri(url, imgUrl)); } else { - card.Thumb = await UploadImageAndSetThumbAsync(new Uri(imgUrl), accessToken); + card.Thumb = await UploadImageAndSetThumbAsync(new Uri(imgUrl)); } _logger.LogInformation("EmbedCard created"); @@ -58,7 +60,7 @@ public async Task Create(Uri url, string accessToken) return card; } - private async Task UploadImageAndSetThumbAsync(Uri imageUrl, string accessToken) + private async Task UploadImageAndSetThumbAsync(Uri imageUrl) { var httpClient = _httpClientFactory.CreateClient(); @@ -76,7 +78,7 @@ public async Task Create(Uri url, string accessToken) }; // Add the Authorization header with the access token to the request message - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); var response = await httpClient.SendAsync(request); diff --git a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs index e7c3669..ea26b61 100644 --- a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs +++ b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs @@ -9,8 +9,8 @@ public class BlueskyIntegrationTest [Fact] public async Task CheckSending() { - var identifier = "devdigest.bsky.social"; - var password = "{password-here}"; + var identifier = "devdigest.today"; + var password = ""; IBlueskyClient client = new BlueskyClient(identifier, password); From a034a26c103c1d91a987aa8f25f7ca5b60a435cf Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 13:28:15 +0300 Subject: [PATCH 09/15] Update auth handling --- src/X.Bluesky/BlueskyClient.cs | 2 +- src/X.Bluesky/EmbedCardBuilder.cs | 8 ++++---- src/X.Bluesky/Models/Session.cs | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index 27674b0..af8ca7e 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -138,7 +138,7 @@ private async Task CreatePost(string text, Uri? url) if (url != null) { - var embedCardBuilder = new EmbedCardBuilder(_httpClientFactory, session.AccessJwt, _logger); + var embedCardBuilder = new EmbedCardBuilder(_httpClientFactory, session, _logger); post.Embed = new Embed { diff --git a/src/X.Bluesky/EmbedCardBuilder.cs b/src/X.Bluesky/EmbedCardBuilder.cs index 4572472..a7e7a89 100644 --- a/src/X.Bluesky/EmbedCardBuilder.cs +++ b/src/X.Bluesky/EmbedCardBuilder.cs @@ -10,13 +10,13 @@ public class EmbedCardBuilder private readonly ILogger _logger; private readonly FileTypeHelper _fileTypeHelper; private readonly IHttpClientFactory _httpClientFactory; - private readonly string _accessToken; + private readonly Session _session; - public EmbedCardBuilder(IHttpClientFactory httpClientFactory, string accessToken, ILogger logger) + public EmbedCardBuilder(IHttpClientFactory httpClientFactory, Session session, ILogger logger) { _logger = logger; _httpClientFactory = httpClientFactory; - _accessToken = accessToken; + _session = session; _fileTypeHelper = new FileTypeHelper(logger); } @@ -78,7 +78,7 @@ public async Task Create(Uri url) }; // Add the Authorization header with the access token to the request message - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _session.AccessJwt); var response = await httpClient.SendAsync(request); diff --git a/src/X.Bluesky/Models/Session.cs b/src/X.Bluesky/Models/Session.cs index 83ee0b3..5ed5049 100644 --- a/src/X.Bluesky/Models/Session.cs +++ b/src/X.Bluesky/Models/Session.cs @@ -6,5 +6,6 @@ namespace X.Bluesky.Models; public record Session { public string AccessJwt { get; set; } = ""; + public string Did { get; set; } = ""; } \ No newline at end of file From fad8008eb5695bdd084adaf83c8c5caa0ba4620e Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 14:32:02 +0300 Subject: [PATCH 10/15] rename LICENSE to LICENSE.md --- LICENSE => LICENSE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE.md (100%) diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md From 873af0ca23eee3d6fd1cef4088a6b9c50ff3040a Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 14:51:21 +0300 Subject: [PATCH 11/15] Update tests and project settings --- src/Directory.Build.props | 2 +- src/X.Bluesky/BlueskyClient.cs | 59 ++++++++----------- src/X.Bluesky/X.Bluesky.csproj | 1 + .../X.Bluesky.Tests/BlueskyIntegrationTest.cs | 16 ++++- 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5a90161..545de36 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ README.md LICENSE.md - x-web.png + Andrew Gubskiy Andrew Gubskiy © 2024 diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index af8ca7e..3c85039 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -21,13 +21,7 @@ public interface IBlueskyClient /// Task Post(string text); - /// - /// Make post with link (page preview will be attached) - /// - /// - /// - /// - Task Post(string text, Uri uri); + Task Authorize(string identifier, string password); } public class BlueskyClient : IBlueskyClient @@ -86,7 +80,17 @@ public BlueskyClient(string identifier, string password) { } - private async Task CreatePost(string text, Uri? url) + public BlueskyClient() + : this(new HttpClientFactory(), "", "") + { + } + + /// + /// Create post + /// + /// Post text + /// + public async Task Post(string text) { var session = await Authorize(_identifier, _password); @@ -108,7 +112,7 @@ private async Task CreatePost(string text, Uri? url) if (facetFeature is FacetFeatureMention facetFeatureMention) { var resolveDid = await _mentionResolver.ResolveMention(facetFeatureMention.Did); - + facetFeatureMention.ResolveDid(resolveDid); } } @@ -124,17 +128,15 @@ private async Task CreatePost(string text, Uri? url) Facets = facets.ToList() }; - if (url == null) - { - //If no link was defined we're trying to get link from facets - var facetFeatureLink = facets - .SelectMany(facet => facet.Features) - .Where(feature => feature is FacetFeatureLink) - .Cast() - .FirstOrDefault(); - - url = facetFeatureLink?.Uri; - } + + //If no link was defined we're trying to get link from facets + var facetFeatureLink = facets + .SelectMany(facet => facet.Features) + .Where(feature => feature is FacetFeatureLink) + .Cast() + .FirstOrDefault(); + + var url = facetFeatureLink?.Uri; if (url != null) { @@ -183,21 +185,6 @@ private async Task CreatePost(string text, Uri? url) response.EnsureSuccessStatusCode(); } - /// - /// Create post - /// - /// Post text - /// - public Task Post(string text) => CreatePost(text, null); - - /// - /// Create post with attached link - /// - /// Post text - /// Link to webpage - /// - public Task Post(string text, Uri uri) => CreatePost(text, uri); - /// /// Authorize in Bluesky /// @@ -206,7 +193,7 @@ private async Task CreatePost(string text, Uri? url) /// /// Instance of authorized session /// - public async Task Authorize(string identifier, string password) + public async Task Authorize(string identifier, string password) { var requestData = new { diff --git a/src/X.Bluesky/X.Bluesky.csproj b/src/X.Bluesky/X.Bluesky.csproj index beb7729..d00b145 100644 --- a/src/X.Bluesky/X.Bluesky.csproj +++ b/src/X.Bluesky/X.Bluesky.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs index ea26b61..05e557b 100644 --- a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs +++ b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs @@ -6,6 +6,20 @@ namespace X.Bluesky.Tests; public class BlueskyIntegrationTest { + [Fact] + public async Task CheckFailedAuth() + { + var identifier = "321"; + var password = "1234"; + + IBlueskyClient client = new BlueskyClient(); + + var session = await client.Authorize(identifier, password); + + + Assert.NotNull(session); + } + [Fact] public async Task CheckSending() { @@ -17,7 +31,7 @@ public async Task CheckSending() var link = new Uri("https://devdigest.today/post/2431"); var text = $"Hello world! This post contains #devdigest and #microsoft also it include {link} which is in middle of post text and @andrew.gubskiy.com mention"; - await client.Post(text, link); + await client.Post(text); Assert.True(true); } From bc6a46722b50c094339e46302c70e92b490caeff Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 15:09:27 +0300 Subject: [PATCH 12/15] Refactor authorization --- src/X.Bluesky/AuthorizationClient.cs | 59 +++++++++++++++++++ src/X.Bluesky/BlueskyClient.cs | 56 +++--------------- src/X.Bluesky/BlueskyHttpClientFactory.cs | 23 ++++++++ src/X.Bluesky/HttpClientFactory.cs | 16 ----- .../X.Bluesky.Tests/BlueskyIntegrationTest.cs | 5 +- 5 files changed, 91 insertions(+), 68 deletions(-) create mode 100644 src/X.Bluesky/AuthorizationClient.cs create mode 100644 src/X.Bluesky/BlueskyHttpClientFactory.cs delete mode 100644 src/X.Bluesky/HttpClientFactory.cs diff --git a/src/X.Bluesky/AuthorizationClient.cs b/src/X.Bluesky/AuthorizationClient.cs new file mode 100644 index 0000000..06059df --- /dev/null +++ b/src/X.Bluesky/AuthorizationClient.cs @@ -0,0 +1,59 @@ +using System.Text; +using Newtonsoft.Json; +using X.Bluesky.Models; + +namespace X.Bluesky; + +public interface IAuthorizationClient +{ + Task GetSession(); +} + +public class AuthorizationClient : IAuthorizationClient +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly string _identifier; + private readonly string _password; + + public AuthorizationClient(string identifier, string password) + : this(new BlueskyHttpClientFactory(), identifier, password) + { + } + + public AuthorizationClient(IHttpClientFactory httpClientFactory, string identifier, string password) + { + _httpClientFactory = httpClientFactory; + _identifier = identifier; + _password = password; + } + + /// + /// Authorize in Bluesky + /// + /// + /// Instance of authorized session + /// + public async Task GetSession() + { + var requestData = new + { + identifier = _identifier, + password = _password + }; + + var json = JsonConvert.SerializeObject(requestData); + + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var httpClient = _httpClientFactory.CreateClient(); + + var uri = "https://bsky.social/xrpc/com.atproto.server.createSession"; + var response = await httpClient.PostAsync(uri, content); + + response.EnsureSuccessStatusCode(); + + var jsonResponse = await response.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject(jsonResponse)!; + } +} \ No newline at end of file diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index 3c85039..caf666f 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -20,15 +20,12 @@ public interface IBlueskyClient /// /// Task Post(string text); - - Task Authorize(string identifier, string password); } public class BlueskyClient : IBlueskyClient { - private readonly string _identifier; - private readonly string _password; private readonly ILogger _logger; + private readonly IAuthorizationClient _authorizationClient; private readonly IMentionResolver _mentionResolver; private readonly IHttpClientFactory _httpClientFactory; private readonly IReadOnlyCollection _languages; @@ -49,8 +46,7 @@ public BlueskyClient( ILogger logger) { _httpClientFactory = httpClientFactory; - _identifier = identifier; - _password = password; + _authorizationClient = new AuthorizationClient(httpClientFactory, identifier, password); _logger = logger; _languages = languages.ToImmutableList(); _mentionResolver = new MentionResolver(_httpClientFactory); @@ -76,15 +72,10 @@ public BlueskyClient( /// Bluesky identifier /// Bluesky application password public BlueskyClient(string identifier, string password) - : this(new HttpClientFactory(), identifier, password) - { - } - - public BlueskyClient() - : this(new HttpClientFactory(), "", "") + : this(new BlueskyHttpClientFactory(), identifier, password) { } - + /// /// Create post /// @@ -92,7 +83,7 @@ public BlueskyClient() /// public async Task Post(string text) { - var session = await Authorize(_identifier, _password); + var session = await _authorizationClient.GetSession(); if (session == null) { @@ -130,14 +121,13 @@ public async Task Post(string text) //If no link was defined we're trying to get link from facets - var facetFeatureLink = facets + var url = facets .SelectMany(facet => facet.Features) .Where(feature => feature is FacetFeatureLink) .Cast() + .Select(f => f.Uri) .FirstOrDefault(); - var url = facetFeatureLink?.Uri; - if (url != null) { var embedCardBuilder = new EmbedCardBuilder(_httpClientFactory, session, _logger); @@ -184,36 +174,4 @@ public async Task Post(string text) // This throws an exception if the HTTP response status is an error code. response.EnsureSuccessStatusCode(); } - - /// - /// Authorize in Bluesky - /// - /// Bluesky identifier - /// Bluesky application password - /// - /// Instance of authorized session - /// - public async Task Authorize(string identifier, string password) - { - var requestData = new - { - identifier = identifier, - password = password - }; - - var json = JsonConvert.SerializeObject(requestData); - - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var httpClient = _httpClientFactory.CreateClient(); - - var uri = "https://bsky.social/xrpc/com.atproto.server.createSession"; - var response = await httpClient.PostAsync(uri, content); - - response.EnsureSuccessStatusCode(); - - var jsonResponse = await response.Content.ReadAsStringAsync(); - - return JsonConvert.DeserializeObject(jsonResponse); - } } \ No newline at end of file diff --git a/src/X.Bluesky/BlueskyHttpClientFactory.cs b/src/X.Bluesky/BlueskyHttpClientFactory.cs new file mode 100644 index 0000000..7d95e99 --- /dev/null +++ b/src/X.Bluesky/BlueskyHttpClientFactory.cs @@ -0,0 +1,23 @@ +using System.Net; + +namespace X.Bluesky; + +public class BlueskyHttpClientFactory : IHttpClientFactory +{ + private readonly HttpClient _client; + + public BlueskyHttpClientFactory() + { + var handler = new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + + _client = new HttpClient(handler); + } + + public HttpClient CreateClient(string name) + { + return _client; + } +} \ No newline at end of file diff --git a/src/X.Bluesky/HttpClientFactory.cs b/src/X.Bluesky/HttpClientFactory.cs deleted file mode 100644 index 72cb72e..0000000 --- a/src/X.Bluesky/HttpClientFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Net; - -namespace X.Bluesky; - -public class HttpClientFactory : IHttpClientFactory -{ - public HttpClient CreateClient(string name) - { - HttpMessageHandler handler = new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate - }; - - return new HttpClient(handler); - } -} \ No newline at end of file diff --git a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs index 05e557b..8aa1aa8 100644 --- a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs +++ b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs @@ -11,11 +11,10 @@ public async Task CheckFailedAuth() { var identifier = "321"; var password = "1234"; - - IBlueskyClient client = new BlueskyClient(); - var session = await client.Authorize(identifier, password); + var authorizationClient = new AuthorizationClient(identifier, password); + var session = await authorizationClient.GetSession(); Assert.NotNull(session); } From c2ea64d68d3e207cebe93bc7757d952db581d767 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 15:13:01 +0300 Subject: [PATCH 13/15] Update --- src/X.Bluesky/BlueskyClient.cs | 2 +- src/X.Bluesky/EmbedCardBuilder.cs | 3 +-- src/X.Bluesky/MentionResolver.cs | 15 ++++++++++----- tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index caf666f..9798e7b 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -134,7 +134,7 @@ public async Task Post(string text) post.Embed = new Embed { - External = await embedCardBuilder.Create(url), + External = await embedCardBuilder.GetEmbedCard(url), Type = "app.bsky.embed.external" }; } diff --git a/src/X.Bluesky/EmbedCardBuilder.cs b/src/X.Bluesky/EmbedCardBuilder.cs index a7e7a89..aa75fb9 100644 --- a/src/X.Bluesky/EmbedCardBuilder.cs +++ b/src/X.Bluesky/EmbedCardBuilder.cs @@ -24,9 +24,8 @@ public EmbedCardBuilder(IHttpClientFactory httpClientFactory, Session session, I /// Create embed card /// /// - /// /// - public async Task Create(Uri url) + public async Task GetEmbedCard(Uri url) { var extractor = new Web.MetaExtractor.Extractor(); var metadata = await extractor.ExtractAsync(url); diff --git a/src/X.Bluesky/MentionResolver.cs b/src/X.Bluesky/MentionResolver.cs index 536eaaf..e3087d5 100644 --- a/src/X.Bluesky/MentionResolver.cs +++ b/src/X.Bluesky/MentionResolver.cs @@ -16,6 +16,16 @@ public class MentionResolver : IMentionResolver { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; + + public MentionResolver() + : this(new BlueskyHttpClientFactory(), new NullLogger()) + { + } + + public MentionResolver(IHttpClientFactory httpClientFactory) + : this(httpClientFactory, new NullLogger()) + { + } public MentionResolver(IHttpClientFactory httpClientFactory, ILogger logger) { @@ -28,11 +38,6 @@ public MentionResolver(IHttpClientFactory httpClientFactory, ILogger()) - { - } - public async Task ResolveMention(string mention) { var httpClient = _httpClientFactory.CreateClient(); diff --git a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs index 8aa1aa8..c7033ce 100644 --- a/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs +++ b/tests/X.Bluesky.Tests/BlueskyIntegrationTest.cs @@ -39,7 +39,7 @@ public async Task CheckSending() public async Task TestResolveMention() { var mention = "@andrew.gubskiy.com"; - var httpClientFactory = new HttpClientFactory(); + var httpClientFactory = new BlueskyHttpClientFactory(); IMentionResolver mentionResolver = new MentionResolver(httpClientFactory); From 440eb2d80c1ce1c31cd00cb87041996c97dcda68 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 15:17:11 +0300 Subject: [PATCH 14/15] Update project icon --- src/Directory.Build.props | 2 +- src/X.Bluesky/X.Bluesky.csproj | 8 ++++++-- x.png | Bin 0 -> 15920 bytes 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 x.png diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 545de36..dcb6acf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ README.md LICENSE.md - + x.png Andrew Gubskiy Andrew Gubskiy © 2024 diff --git a/src/X.Bluesky/X.Bluesky.csproj b/src/X.Bluesky/X.Bluesky.csproj index d00b145..188ad72 100644 --- a/src/X.Bluesky/X.Bluesky.csproj +++ b/src/X.Bluesky/X.Bluesky.csproj @@ -5,7 +5,7 @@ Simple client for posting to Bluesky enable enable - bluesky, social networks + bluesky, social networks net8.0;net6.0;netstandard2.1 default @@ -14,7 +14,7 @@ - + @@ -26,4 +26,8 @@ + + + + diff --git a/x.png b/x.png new file mode 100644 index 0000000000000000000000000000000000000000..dec1b4fd06a045ba477dd50cf3c7fd3c7957d878 GIT binary patch literal 15920 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 Do!C!6 literal 0 HcmV?d00001 From 4ceea0d0749fc20956db5d9f0f4fb1dbcfe28bdd Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Sep 2024 15:27:49 +0300 Subject: [PATCH 15/15] Update IBlueskyClient --- README.md | 5 +++-- src/X.Bluesky/BlueskyClient.cs | 33 ++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2aff13a..20d79e0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ var password = "your-password-here"; IBlueskyClient client = new BlueskyClient(identifier, password); -var link = new Uri("https://yourlink.com/post/123"); +await client.Post($"Read this post from #devdigest: https://yourlink.com/post/123"); -await client.Post("Hello world!", link); +await client.Post($"Read this post!", new Uri("https://yourlink.com/post/123"); ``` + diff --git a/src/X.Bluesky/BlueskyClient.cs b/src/X.Bluesky/BlueskyClient.cs index 9798e7b..51adaad 100644 --- a/src/X.Bluesky/BlueskyClient.cs +++ b/src/X.Bluesky/BlueskyClient.cs @@ -20,6 +20,8 @@ public interface IBlueskyClient /// /// Task Post(string text); + + Task Post(string text, Uri uri); } public class BlueskyClient : IBlueskyClient @@ -75,13 +77,24 @@ public BlueskyClient(string identifier, string password) : this(new BlueskyHttpClientFactory(), identifier, password) { } + + /// + /// Create post + /// + /// Post text + /// + public Task Post(string text) => CreatePost(text, null); + public Task Post(string text, Uri uri) => CreatePost(text, uri); + + /// /// Create post /// /// Post text + /// /// - public async Task Post(string text) + private async Task CreatePost(string text, Uri? url) { var session = await _authorizationClient.GetSession(); @@ -119,14 +132,16 @@ public async Task Post(string text) Facets = facets.ToList() }; - - //If no link was defined we're trying to get link from facets - var url = facets - .SelectMany(facet => facet.Features) - .Where(feature => feature is FacetFeatureLink) - .Cast() - .Select(f => f.Uri) - .FirstOrDefault(); + 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() + .Select(f => f.Uri) + .FirstOrDefault(); + } if (url != null) {