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
1 change: 1 addition & 0 deletions samples/AuthTest/AuthTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<LexiconResolve Include="app.bsky.actor.getProfile" />
<LexiconResolve Include="app.bsky.actor.getPreferences" />
<LexiconResolve Include="app.bsky.feed.getTimeline" />
<LexiconResolve Include="app.bsky.feed.getPosts" />
<LexiconResolve Include="app.bsky.feed.post" />
<LexiconResolve Include="app.bsky.notification.listNotifications" />
<LexiconResolve Include="com.atproto.repo.createRecord" />
Expand Down
67 changes: 56 additions & 11 deletions samples/AuthTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,13 @@ async Task AuthenticatedMenuAsync(IATProtoClient client, string? handle, string
Console.WriteLine(" 1. Get my profile");
Console.WriteLine(" 2. Get my preferences");
Console.WriteLine(" 3. Get my timeline");
Console.WriteLine(" 4. Get notifications");
Console.WriteLine(" 5. Create a post");
Console.WriteLine(" 6. Delete a post");
Console.WriteLine(" 7. Save session");
Console.WriteLine(" 8. Show session tokens");
Console.WriteLine(" 9. Exit");
Console.WriteLine(" 4. Get posts by URIs");
Console.WriteLine(" 5. Get notifications");
Console.WriteLine(" 6. Create a post");
Console.WriteLine(" 7. Delete a post");
Console.WriteLine(" 8. Save session");
Console.WriteLine(" 9. Show session tokens");
Console.WriteLine(" 10. Exit");
Console.Write("> ");

var choice = Console.ReadLine()?.Trim();
Expand All @@ -288,21 +289,24 @@ async Task AuthenticatedMenuAsync(IATProtoClient client, string? handle, string
await GetTimelineAsync(client);
break;
case "4":
await GetNotificationsAsync(client);
await GetPostsAsync(client);
break;
case "5":
await CreatePostAsync(client, did);
await GetNotificationsAsync(client);
break;
case "6":
await DeletePostAsync(client, did);
await CreatePostAsync(client, did);
break;
case "7":
SaveCurrentSession(client);
await DeletePostAsync(client, did);
break;
case "8":
await ShowSessionTokensAsync(client);
SaveCurrentSession(client);
break;
case "9":
await ShowSessionTokensAsync(client);
break;
case "10":
return;
default:
Console.WriteLine("Invalid choice.");
Expand Down Expand Up @@ -356,6 +360,47 @@ async Task GetTimelineAsync(IATProtoClient client)
}
}

async Task GetPostsAsync(IATProtoClient client)
{
Console.WriteLine("Enter AT URIs (one per line, blank line to finish):");
var uris = new List<ATUri>();
while (true)
{
Console.Write("> ");
var line = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(line))
break;

var uri = new ATUri(line);
if (!uri.IsValid)
{
Console.WriteLine($" Skipped invalid URI: {line}");
continue;
}
uris.Add(uri);
}

if (uris.Count == 0)
{
Console.WriteLine("No URIs provided.");
return;
}

// Demonstrates the fix for https://github.com/drasticactions/CarpaNet/issues/12 —
// passing multiple URIs serializes as repeated query parameters (?uris=a&uris=b).
var parameters = new GetPostsParameters { Uris = uris };
var response = await client.AppBskyFeedGetPostsAsync(parameters);

Console.WriteLine($" Returned posts: {response.Posts?.Count ?? 0}");
if (response.Posts != null)
{
foreach (var post in response.Posts)
{
Console.WriteLine($" - @{post.Author?.Handle}: {post.Uri}");
}
}
}

async Task GetNotificationsAsync(IATProtoClient client)
{
var parameters = new ListNotificationsParameters { Limit = 10 };
Expand Down
18 changes: 9 additions & 9 deletions src/CarpaNet.OAuth/ATProtoOAuthClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,35 +101,35 @@ internal ATProtoOAuthClient(
/// <inheritdoc/>
public async Task<TOutput> GetAsync<TOutput>(
string nsid,
IReadOnlyDictionary<string, string>? parameters = null,
IEnumerable<KeyValuePair<string, string>>? parameters = null,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
_logger.LogDebug("OAuth GET {Nsid}", nsid);

var url = (await XrpcHttpHandler.BuildUrlAsync(BaseUrl, nsid, parameters, _identityResolver, cancellationToken).ConfigureAwait(false)).ToString();
var url = (await XrpcHttpHandler.BuildUrlAsync(BaseUrl, nsid, parameters, _identityResolver, _logger, cancellationToken).ConfigureAwait(false)).ToString();
using var request = await _tokenProvider.CreateDPoPRequestAsync(HttpMethod.Get, url).ConfigureAwait(false);
XrpcHttpHandler.AddCommonHeaders(request, null, LabelerDids);

var response = await SendWithRetryAsync(request, url, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, _logger, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public async Task<TOutput> GetAsync<TOutput>(
string nsid,
string proxyServiceDid,
IReadOnlyDictionary<string, string>? parameters = null,
IEnumerable<KeyValuePair<string, string>>? parameters = null,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();

var url = (await XrpcHttpHandler.BuildUrlAsync(BaseUrl, nsid, parameters, _identityResolver, cancellationToken).ConfigureAwait(false)).ToString();
var url = (await XrpcHttpHandler.BuildUrlAsync(BaseUrl, nsid, parameters, _identityResolver, _logger, cancellationToken).ConfigureAwait(false)).ToString();
using var request = await _tokenProvider.CreateDPoPRequestAsync(HttpMethod.Get, url).ConfigureAwait(false);
XrpcHttpHandler.AddCommonHeaders(request, proxyServiceDid, LabelerDids);

var response = await SendWithRetryAsync(request, url, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, _logger, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
Expand All @@ -153,7 +153,7 @@ public async Task<TOutput> PostAsync<TInput, TOutput>(
}

var response = await SendWithRetryAsync(request, url, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, _logger, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
Expand All @@ -177,13 +177,13 @@ public async Task<TOutput> PostAsync<TInput, TOutput>(
}

var response = await SendWithRetryAsync(request, url, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
return await XrpcHttpHandler.ProcessResponseAsync<TOutput>(response, _jsonOptions, _logger, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public IAsyncEnumerable<TMessage> SubscribeAsync<TMessage>(
string nsid,
IReadOnlyDictionary<string, string>? parameters = null,
IEnumerable<KeyValuePair<string, string>>? parameters = null,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
Expand Down
52 changes: 27 additions & 25 deletions src/CarpaNet.SourceGen/Generation/ApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,27 @@ public static void GenerateParametersClass(
new List<string>());
}

// Generate ToDictionary method for converting to query parameters
GenerateToDictionaryMethod(sb, def.Parameters.Properties, requiredProps);
// Generate ToQueryParameters method for converting to query parameters
GenerateToQueryParametersMethod(sb, def.Parameters.Properties, requiredProps);

sb.CloseBrace();
}

/// <summary>
/// Generates the ToDictionary method for a parameters class.
/// Generates the ToQueryParameters method for a parameters class.
/// </summary>
private static void GenerateToDictionaryMethod(
private static void GenerateToQueryParametersMethod(
SourceBuilder sb,
Dictionary<string, LexiconDefinition> properties,
List<string> requiredProps)
{
sb.AppendLine();
sb.WriteSummary("Converts the parameters to a dictionary for query string serialization.");
sb.AppendLine("public System.Collections.Generic.IReadOnlyDictionary<string, string> ToDictionary()");
sb.WriteSummary("Converts the parameters to a sequence of key/value pairs for query string serialization. " +
"Returns IEnumerable<KeyValuePair<string,string>> rather than a dictionary so that array properties " +
"(e.g. uris on app.bsky.feed.getPosts) can emit repeated query parameters.");
sb.AppendLine("public System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, string>> ToQueryParameters()");
sb.OpenBrace();
sb.AppendLine("var dict = new System.Collections.Generic.Dictionary<string, string>();");
sb.AppendLine("var list = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, string>>();");
Comment thread
drasticactions marked this conversation as resolved.
sb.AppendLine();

foreach (var prop in properties)
Expand All @@ -88,11 +90,11 @@ private static void GenerateToDictionaryMethod(
sb.OpenBrace();
if (prop.Value.Items?.Type == "string")
{
sb.AppendLine($"dict.Add(\"{jsonName}\", item);");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", item));");
}
else
{
sb.AppendLine($"dict.Add(\"{jsonName}\", item?.ToString() ?? \"\");");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", item?.ToString() ?? \"\"));");
}
sb.CloseBrace();
sb.CloseBrace();
Expand All @@ -102,13 +104,13 @@ private static void GenerateToDictionaryMethod(
{
if (isRequired)
{
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.ToString().ToLowerInvariant();");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.ToString().ToLowerInvariant()));");
}
else
{
sb.AppendLine($"if ({propName} != null)");
sb.OpenBrace();
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.Value.ToString().ToLowerInvariant();");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.Value.ToString().ToLowerInvariant()));");
sb.CloseBrace();
}
}
Expand All @@ -117,13 +119,13 @@ private static void GenerateToDictionaryMethod(
{
if (isRequired)
{
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.ToString();");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.ToString()));");
}
else
{
sb.AppendLine($"if ({propName} != null)");
sb.OpenBrace();
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.Value.ToString();");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.Value.ToString()));");
sb.CloseBrace();
}
}
Expand All @@ -137,13 +139,13 @@ private static void GenerateToDictionaryMethod(
{
if (isRequired)
{
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.ToString(\"o\")!;");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.ToString(\"o\")!));");
}
else
{
sb.AppendLine($"if ({propName} != null)");
sb.OpenBrace();
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.Value.ToString(\"o\")!;");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.Value.ToString(\"o\")!));");
sb.CloseBrace();
}
}
Expand All @@ -152,13 +154,13 @@ private static void GenerateToDictionaryMethod(
{
if (isRequired)
{
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.ToString()!;");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.ToString()!));");
}
else
{
sb.AppendLine($"if ({propName} != null)");
sb.OpenBrace();
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.ToString()!;");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.ToString()!));");
sb.CloseBrace();
}
}
Expand All @@ -167,13 +169,13 @@ private static void GenerateToDictionaryMethod(
{
if (isRequired)
{
sb.AppendLine($"dict[\"{jsonName}\"] = {propName};");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}));");
}
else
{
sb.AppendLine($"if ({propName} != null)");
sb.OpenBrace();
sb.AppendLine($"dict[\"{jsonName}\"] = {propName};");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}));");
sb.CloseBrace();
}
}
Expand All @@ -184,22 +186,22 @@ private static void GenerateToDictionaryMethod(
if (isRequired)
{
// Required reference type - ToString() is safe
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.ToString()!;");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.ToString()!));");
}
else
{
// Optional reference type - check for null first
sb.AppendLine($"if ({propName} != null)");
sb.OpenBrace();
sb.AppendLine($"dict[\"{jsonName}\"] = {propName}.ToString()!;");
sb.AppendLine($"list.Add(new System.Collections.Generic.KeyValuePair<string, string>(\"{jsonName}\", {propName}.ToString()!));");
sb.CloseBrace();
}
}

sb.AppendLine();
}

sb.AppendLine("return dict;");
sb.AppendLine("return list;");
sb.CloseBrace();
}

Expand Down Expand Up @@ -477,7 +479,7 @@ public static void GenerateQueryExtension(
sb.Indent();
sb.AppendLine($"\"{currentNsid}\",");
sb.AppendLine($"{proxyServiceDid},");
sb.AppendLine(hasParameters ? "parameters?.ToDictionary()," : "null,");
sb.AppendLine(hasParameters ? "parameters?.ToQueryParameters()," : "null,");
sb.AppendLine("cancellationToken);");
sb.Unindent();
}
Expand All @@ -486,7 +488,7 @@ public static void GenerateQueryExtension(
sb.AppendLine($"return await client.GetAsync<{outputType}>(");
sb.Indent();
sb.AppendLine($"\"{currentNsid}\",");
sb.AppendLine(hasParameters ? "parameters?.ToDictionary()," : "null,");
sb.AppendLine(hasParameters ? "parameters?.ToQueryParameters()," : "null,");
sb.AppendLine("cancellationToken);");
sb.Unindent();
}
Expand Down Expand Up @@ -610,7 +612,7 @@ public static void GenerateSubscriptionExtension(
sb.AppendLine($"return client.SubscribeAsync<{currentNamespace}.{interfaceName}>(");
sb.Indent();
sb.AppendLine($"\"{currentNsid}\",");
sb.AppendLine(hasParameters ? "parameters?.ToDictionary()," : "null,");
sb.AppendLine(hasParameters ? "parameters?.ToQueryParameters()," : "null,");
sb.AppendLine("cancellationToken);");
sb.Unindent();
sb.CloseBrace();
Expand Down
Loading
Loading