Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
34 changes: 34 additions & 0 deletions src/Resend/BroadcastClickedLink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;

namespace Resend;

/// <summary>
/// A single clicked link in a broadcast clicked-links list response.
/// </summary>
public class BroadcastClickedLink
{
/// <summary>
/// An opaque cursor for this row, used only for pagination.
/// It does not identify any entity in Resend.
/// </summary>
[JsonPropertyName( "id" )]
public string Id { get; set; } = default!;

/// <summary>
/// The URL that was clicked.
/// </summary>
[JsonPropertyName( "url" )]
public string Url { get; set; } = default!;

/// <summary>
/// Total number of clicks on this URL.
/// </summary>
[JsonPropertyName( "clicks" )]
public int Clicks { get; set; }

/// <summary>
/// Number of unique clicks on this URL.
/// </summary>
[JsonPropertyName( "unique_clicks" )]
public int UniqueClicks { get; set; }
}
9 changes: 9 additions & 0 deletions src/Resend/IResend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,15 @@ public interface IResend
/// <returns>Response.</returns>
Task<ResendResponse> BroadcastDeleteAsync( Guid broadcastId, CancellationToken cancellationToken = default );

/// <summary>
/// Lists the links clicked in a broadcast.
/// </summary>
/// <param name="broadcastId">Broadcast identifier.</param>
/// <param name="query">Pagination query.</param>
/// <param name="cancellationToken">Cancelation token.</param>
/// <returns>Page of clicked links.</returns>
Task<ResendResponse<PaginatedResult<BroadcastClickedLink>>> BroadcastClickedLinksAsync( Guid broadcastId, PaginatedQuery? query = null, CancellationToken cancellationToken = default );

#endregion

#region Receiving
Expand Down
28 changes: 28 additions & 0 deletions src/Resend/ResendClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,34 @@ public ResendClient( IOptionsSnapshot<ResendClientOptions> options, HttpClient h
}


/// <inheritdoc/>
public Task<ResendResponse<PaginatedResult<BroadcastClickedLink>>> BroadcastClickedLinksAsync( Guid broadcastId, PaginatedQuery? query = null, CancellationToken cancellationToken = default )
{
var baseUrl = $"/broadcasts/{broadcastId}/clicked-links";
var url = baseUrl;

if ( query != null )
{
var qs = new Dictionary<string, string?>();

if ( query.Limit.HasValue == true )
qs.Add( "limit", query.Limit.Value.ToString() );

if ( query.Before != null )
qs.Add( "before", query.Before );

if ( query.After != null )
qs.Add( "after", query.After );

url = QueryHelpers.AddQueryString( baseUrl, qs );
}

var req = new HttpRequestMessage( HttpMethod.Get, url );

return Execute<PaginatedResult<BroadcastClickedLink>, PaginatedResult<BroadcastClickedLink>>( req, ( x ) => x, cancellationToken );
}


/// <summary />
private async Task<ResendResponse> Execute( HttpRequestMessage req, CancellationToken cancellationToken )
{
Expand Down
35 changes: 35 additions & 0 deletions tests/Resend.Tests/ResendClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,4 +499,39 @@ public async Task BroadcastDelete()

Assert.NotNull( resp );
}


/// <summary/>
[Fact]
public async Task BroadcastClickedLinks()
{
var broadcastId = Guid.NewGuid();

var resp = await _resend.BroadcastClickedLinksAsync( broadcastId );

Assert.NotNull( resp );
Assert.True( resp.Success );
Assert.False( resp.Content.HasMore );
Assert.Equal( 2, resp.Content.Data.Count );
Assert.Equal( "https://resend.com/pricing", resp.Content.Data[ 0 ].Url );
Assert.Equal( 42, resp.Content.Data[ 0 ].Clicks );
Assert.Equal( 30, resp.Content.Data[ 0 ].UniqueClicks );
}


/// <summary/>
[Fact]
public async Task BroadcastClickedLinks_WithQuery()
{
var broadcastId = Guid.NewGuid();

var resp = await _resend.BroadcastClickedLinksAsync( broadcastId, new PaginatedQuery()
{
Limit = 10,
After = "cursor-value",
} );

Assert.NotNull( resp );
Assert.True( resp.Success );
}
}
36 changes: 36 additions & 0 deletions tools/Resend.ApiServer/Controllers/BroadcastController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,40 @@ public ListOf<Broadcast> BroadcastList()
Data = list,
};
}


/// <summary />
[HttpGet]
[Route( "broadcasts/{broadcastId}/clicked-links" )]
public PaginatedResult<BroadcastClickedLink> BroadcastClickedLinks(
[FromRoute] Guid broadcastId,
[FromQuery] string? limit = null,
[FromQuery] string? before = null,
[FromQuery] string? after = null
)
{
_logger.LogDebug( "BroadcastClickedLinks" );

return new PaginatedResult<BroadcastClickedLink>()
{
HasMore = false,
Data =
[
new BroadcastClickedLink()
{
Id = "b2Zmc2V0OjA",
Url = "https://resend.com/pricing",
Clicks = 42,
UniqueClicks = 30,
},
new BroadcastClickedLink()
{
Id = "b2Zmc2V0OjE",
Url = "https://resend.com/docs",
Clicks = 17,
UniqueClicks = 15,
},
],
};
}
}
66 changes: 66 additions & 0 deletions tools/Resend.Cli/Broadcast/BroadcastClickedLinksCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using McMaster.Extensions.CommandLineUtils;
using Spectre.Console;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;

namespace Resend.Cli.Broadcast;

/// <summary />
[Command( "clicked-links", Description = "Lists the clicked links of a broadcast" )]
public class BroadcastClickedLinksCommand
{
private readonly IResend _resend;

/// <summary />
[Argument( 0, Description = "Broadcast identifier" )]
[Required]
public Guid? BroadcastId { get; set; }

/// <summary />
[Option( "-j|--json", CommandOptionType.NoValue, Description = "Emit output as JSON array" )]
public bool InJson { get; set; }


/// <summary />
public BroadcastClickedLinksCommand( IResend resend )
{
_resend = resend;
}


/// <summary />
public async Task<int> OnExecuteAsync()
{
var res = await _resend.BroadcastClickedLinksAsync( this.BroadcastId!.Value );
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
var rows = res.Content.Data;

if ( this.InJson == true )
{
var jso = new JsonSerializerOptions() { WriteIndented = true };
var json = JsonSerializer.Serialize( rows, jso );

Console.WriteLine( json );
}
else
{
var table = new Table();
table.Border = TableBorder.SimpleHeavy;
table.AddColumn( "Url" );
table.AddColumn( "Clicks" );
table.AddColumn( "Unique clicks" );

foreach ( var c in rows )
{
table.AddRow(
new Markup( c.Url ),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
new Markup( c.Clicks.ToString() ),
new Markup( c.UniqueClicks.ToString() )
);
}

AnsiConsole.Write( table );
}

return 0;
}
}
1 change: 1 addition & 0 deletions tools/Resend.Cli/BroadcastCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Resend.Cli;
[Command( "broadcast", Description = "Broadcast management" )]
[Subcommand( typeof( Broadcast.BroadcastAddCommand ))]
[Subcommand( typeof( Broadcast.BroadcastCancelCommand ) )]
[Subcommand( typeof( Broadcast.BroadcastClickedLinksCommand ) )]
[Subcommand( typeof( Broadcast.BroadcastDeleteCommand ) )]
[Subcommand( typeof( Broadcast.BroadcastListCommand ) )]
[Subcommand( typeof( Broadcast.BroadcastRetrieveCommand ))]
Expand Down
Loading