diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs index 83b8cc6..005bfc0 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs @@ -17,7 +17,7 @@ partial class BlueskyApiClient { public async Task GetAuthorAsync(string accessToken, string identifier) { - var timelineUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.ProfilePath}?actor={identifier}"; + var timelineUrl = $"{_baseUrl}/{UrlConstants.ProfilePath}?actor={identifier}"; HttpRequestMessage message = new(HttpMethod.Get, timelineUrl); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); @@ -56,7 +56,7 @@ public async Task> GetSuggestedPeopleAsync( count = 100; } - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.SuggestedPeoplePath}?limit={count}"; + var url = $"{_baseUrl}/{UrlConstants.SuggestedPeoplePath}?limit={count}"; if (cursor is { Length: > 0 }) { url += $"&cursor={cursor}"; @@ -78,7 +78,7 @@ public async Task FollowActorAsync( string subjectDid, CancellationToken ct) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.CreateRecordPath}"; + var url = $"{_baseUrl}/{UrlConstants.CreateRecordPath}"; FollowRecordBody body = new() { diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Blob.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Blob.cs index 1600441..e509f1c 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Blob.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Blob.cs @@ -14,7 +14,7 @@ partial class BlueskyApiClient { public async Task UploadBlobAsync(string accessToken, byte[] blob, string mimeType) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.UploadBlobPath}"; + var url = $"{_baseUrl}/{UrlConstants.UploadBlobPath}"; HttpRequestMessage message = new(HttpMethod.Post, url) { diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.FeedGenerators.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.FeedGenerators.cs index 26d94d0..e9810ac 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.FeedGenerators.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.FeedGenerators.cs @@ -26,7 +26,7 @@ public async Task>> GetFeedGeneratorsAsync( string combined = string.Join("&", feedParameters); - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.FeedGeneratorsPath}?{combined}"; + var url = $"{_baseUrl}/{UrlConstants.FeedGeneratorsPath}?{combined}"; HttpRequestMessage message = new(HttpMethod.Get, url); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); @@ -46,7 +46,7 @@ public async Task> GetSuggestedFeedGeneratorsAsync( CancellationToken ct, string? cursor = null) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.SuggestedFeedsPath}"; + var url = $"{_baseUrl}/{UrlConstants.SuggestedFeedsPath}"; if (cursor is { Length: > 0 }) { url += $"?cursor={cursor}"; diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Feeds.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Feeds.cs index 5e9f506..88d8661 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Feeds.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Feeds.cs @@ -24,7 +24,7 @@ public async Task> GetFeedAsync( { ct.ThrowIfCancellationRequested(); - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.FeedPath}?feed={atUri}"; + var url = $"{_baseUrl}/{UrlConstants.FeedPath}?feed={atUri}"; if (cursor is { Length: > 0 } cursorParameter) { url += $"&cursor={cursorParameter}"; @@ -45,7 +45,7 @@ public async Task> GetFeedAsync( public async Task GetTimelineAsync(string accesstoken, string? cursor = null) { - var timelineUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.TimelinePath}"; + var timelineUrl = $"{_baseUrl}/{UrlConstants.TimelinePath}"; if (cursor is { Length: > 0 } cursorParameter) { timelineUrl += $"?cursor={cursorParameter}"; @@ -74,7 +74,7 @@ public async Task GetTimelineAsync(string accesstoken, string? cur public async Task> GetAuthorFeedAsync(string accesstoken, string handle) { - var feedUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.AuthorFeedPath}?actor={handle}"; + var feedUrl = $"{_baseUrl}/{UrlConstants.AuthorFeedPath}?actor={handle}"; HttpRequestMessage message = new(HttpMethod.Get, feedUrl); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accesstoken); diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Notifications.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Notifications.cs index 5840b84..09af1d7 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Notifications.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Notifications.cs @@ -16,7 +16,7 @@ partial class BlueskyApiClient { public async Task> GetNotificationsAsync(string accessToken) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.NotificationsPath}"; + var url = $"{_baseUrl}/{UrlConstants.NotificationsPath}"; HttpRequestMessage message = new(HttpMethod.Get, url); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); @@ -35,7 +35,7 @@ public async Task> GetNotificationsAsync(string acce /// public async Task UpdateSeenAsync(string accessToken, CancellationToken ct) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.UpdateSeenPath}"; + var url = $"{_baseUrl}/{UrlConstants.UpdateSeenPath}"; UpdateSeenBody body = new() { SeenAt = DateTime.Now.ToUniversalTime().ToString("O") @@ -52,7 +52,7 @@ public async Task UpdateSeenAsync(string accessToken, CancellationToken /// public async Task> GetUnreadCountAsync(string accessToken, CancellationToken ct) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.UnreadCountPath}"; + var url = $"{_baseUrl}/{UrlConstants.UnreadCountPath}"; HttpRequestMessage message = new(HttpMethod.Get, url); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Posts.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Posts.cs index 5bcd2ee..2b15de0 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Posts.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Posts.cs @@ -22,7 +22,7 @@ public async Task> GetPostsAsync(string accessToken, IRe return []; } - var timelineUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.PostsPath}?uris={string.Join(",", atUriList)}"; + var timelineUrl = $"{_baseUrl}/{UrlConstants.PostsPath}?uris={string.Join(",", atUriList)}"; HttpRequestMessage message = new(HttpMethod.Get, timelineUrl); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); @@ -48,7 +48,7 @@ public async Task> GetPostsAsync(string accessToken, IRe /// public async Task SubmitPostAsync(string accessToken, string handle, SubmissionRecord record, RecordType recordType) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.CreateRecordPath}"; + var url = $"{_baseUrl}/{UrlConstants.CreateRecordPath}"; CreateRecordBody body = new() { @@ -91,7 +91,7 @@ public async Task> GetPostsAsync(string accessToken, IRe /// public async Task SubmitPostUndoAsync(string accessToken, string handle, string rkey, RecordType recordType, CancellationToken cancellationToken) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.DeleteRecordPath}"; + var url = $"{_baseUrl}/{UrlConstants.DeleteRecordPath}"; DeleteRecordBody body = new() { diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Preferences.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Preferences.cs index 582c0ea..7a0b683 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Preferences.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Preferences.cs @@ -21,7 +21,7 @@ public async Task>> GetPreferencesAsync(str { ct.ThrowIfCancellationRequested(); - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.PreferencesPath}"; + var url = $"{_baseUrl}/{UrlConstants.PreferencesPath}"; HttpRequestMessage message = new(HttpMethod.Get, url); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Search.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Search.cs index 8a471d9..195c1c1 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Search.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Search.cs @@ -20,7 +20,7 @@ public async Task> SearchFeedsAsync( CancellationToken ct, string? cursor = null) { - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.SearchFeedsPath}?query={HttpUtility.UrlEncode(query)}"; + var url = $"{_baseUrl}/{UrlConstants.SearchFeedsPath}?query={HttpUtility.UrlEncode(query)}"; if (cursor is not null) { @@ -44,7 +44,7 @@ public async Task> SearchActorsAsync( string? cursor = null) { // Ref: https://docs.bsky.app/docs/api/app-bsky-actor-search-actors - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.SearchActorsPath}?q={HttpUtility.UrlEncode(query)}"; + var url = $"{_baseUrl}/{UrlConstants.SearchActorsPath}?q={HttpUtility.UrlEncode(query)}"; if (cursor is not null) { @@ -70,7 +70,7 @@ public async Task> SearchActorsAsync( // Ref: https://docs.bsky.app/docs/api/app-bsky-feed-search-posts options ??= new(); - var url = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.SearchPostsPath}?q={HttpUtility.UrlEncode(query)}"; + var url = $"{_baseUrl}/{UrlConstants.SearchPostsPath}?q={HttpUtility.UrlEncode(query)}"; if (cursor is not null) { diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs index c55773d..8e7cf02 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs @@ -16,19 +16,22 @@ namespace Bluesky.NET.ApiClients; public partial class BlueskyApiClient : IBlueskyApiClient { private readonly HttpClient _httpClient = new(); + private string _baseUrl = "https://bsky.social"; - public async Task> RefreshAsync(string refreshToken) + public async Task> RefreshAsync(string refreshToken, string baseUrl = UrlConstants.BlueskyBaseUrl) { - var refreshUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.RefreshAuthPath}"; + _baseUrl = baseUrl; + var refreshUrl = $"{baseUrl}/{UrlConstants.RefreshAuthPath}"; HttpRequestMessage message = new(HttpMethod.Post, refreshUrl); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", refreshToken); return await PostAuthMessageAsync(message); } /// - public async Task> AuthenticateAsync(string identifer, string appPassword) + public async Task> AuthenticateAsync(string identifer, string appPassword, string baseUrl = UrlConstants.BlueskyBaseUrl) { - var authUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.AuthPath}"; + _baseUrl = baseUrl; + var authUrl = $"{baseUrl}/{UrlConstants.AuthPath}"; var requestBody = new AuthRequestBody { diff --git a/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs b/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs index 58f3272..6fb9cc4 100644 --- a/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs +++ b/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs @@ -16,9 +16,9 @@ public interface IBlueskyApiClient /// The user's handle or email address or DID. /// An app password provided by the user. /// An . - Task> AuthenticateAsync(string identifier, string appPassword); + Task> AuthenticateAsync(string identifier, string appPassword, string baseUrl = UrlConstants.BlueskyBaseUrl); Task GetTimelineAsync(string accesstoken, string? cursor = null); - Task> RefreshAsync(string refreshToken); + Task> RefreshAsync(string refreshToken, string baseUrl = UrlConstants.BlueskyBaseUrl); Task GetAuthorAsync(string accessToken, string identifier); diff --git a/src/BlueskyClient.Uwp/Strings/en-US/Resources.resw b/src/BlueskyClient.Uwp/Strings/en-US/Resources.resw index 29ba7f2..6c63f79 100644 --- a/src/BlueskyClient.Uwp/Strings/en-US/Resources.resw +++ b/src/BlueskyClient.Uwp/Strings/en-US/Resources.resw @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Advanced + What's an App Password? @@ -192,6 +195,9 @@ Your special App Password + + Custom PDS URL + People diff --git a/src/BlueskyClient.Uwp/Views/SignInPage.xaml b/src/BlueskyClient.Uwp/Views/SignInPage.xaml index 2345347..2ff8770 100644 --- a/src/BlueskyClient.Uwp/Views/SignInPage.xaml +++ b/src/BlueskyClient.Uwp/Views/SignInPage.xaml @@ -63,6 +63,27 @@ Command="{x:Bind ViewModel.SignInCommand}" Content="{x:Bind strings:Resources.SignInText}" IsEnabled="{x:Bind ex:UIExtensions.Not(ViewModel.SigningIn), Mode=OneWay}" /> + + + + + + + > TrySilentSignInAsync() string? storedAppPassword = _secureCredentialStorage.GetCredential(AppPasswordCredentialKey(storedDid)); if (storedAppPassword is { Length: > 0 }) { - result = await SignInWithValidatedCredentialsAsync(storedDid, storedAppPassword); + result = await SignInWithValidatedCredentialsAsync(storedDid, storedAppPassword, _secureCredentialStorage.GetCredential(BaseURLCredentialKey(storedDid))); } return result; @@ -67,6 +68,7 @@ public void SignOut() { _secureCredentialStorage.SetCredential(storedDid, string.Empty); _secureCredentialStorage.SetCredential(AppPasswordCredentialKey(storedDid), string.Empty); + _secureCredentialStorage.SetCredential(BaseURLCredentialKey(storedDid), string.Empty); } _userSettings.Set(UserSettingsConstants.LocalUserIdKey, string.Empty); @@ -77,7 +79,7 @@ public void SignOut() } /// - public async Task> SignInAsync(string rawUserHandleOrEmail, string rawPassword) + public async Task> SignInAsync(string rawUserHandleOrEmail, string rawPassword, string? baseUrl) { var userHandleOrEmail = rawUserHandleOrEmail.Trim().TrimStart('@'); var password = rawPassword.Trim(); @@ -87,7 +89,7 @@ public async Task> SignInAsync(string rawUserHandleOrEmail, return Result.Fail("Empty identifier or password"); } - return await SignInWithValidatedCredentialsAsync(userHandleOrEmail, password); + return await SignInWithValidatedCredentialsAsync(userHandleOrEmail, password, baseUrl ?? "bsky.social"); } public async Task> TryGetFreshTokenAsync() @@ -142,9 +144,9 @@ private void UpdateStoredToken(AuthResponse response) } } - private async Task> SignInWithValidatedCredentialsAsync(string identifier, string password) + private async Task> SignInWithValidatedCredentialsAsync(string identifier, string password, string? baseUrl) { - Result result = await _apiClient.AuthenticateAsync(identifier, password); + Result result = await _apiClient.AuthenticateAsync(identifier, password, baseUrl ?? "bsky.social"); if (result.IsSuccess) { @@ -154,6 +156,9 @@ private async Task> SignInWithValidatedCredentialsAsync(str { _userSettings.Set(UserSettingsConstants.SignedInDIDKey, did); _secureCredentialStorage.SetCredential(AppPasswordCredentialKey(did), password); + _secureCredentialStorage.SetCredential( + BaseURLCredentialKey(did), + baseUrl ?? UrlConstants.BlueskyBaseUrl); } } @@ -162,4 +167,5 @@ private async Task> SignInWithValidatedCredentialsAsync(str private static string AppPasswordCredentialKey(string did) => $"{did}-appPassword"; + private static string BaseURLCredentialKey(string did) => $"{did}-baseUrl"; } diff --git a/src/BlueskyClient/Services/IAuthenticationService.cs b/src/BlueskyClient/Services/IAuthenticationService.cs index 89b47b8..7811ee2 100644 --- a/src/BlueskyClient/Services/IAuthenticationService.cs +++ b/src/BlueskyClient/Services/IAuthenticationService.cs @@ -6,7 +6,7 @@ namespace BlueskyClient.Services; public interface IAuthenticationService { - Task> SignInAsync(string rawUserHandle, string rawPassword); + Task> SignInAsync(string rawUserHandle, string rawPassword, string? baseUrl); void SignOut(); Task> TryGetFreshTokenAsync(); Task> TrySilentSignInAsync(); diff --git a/src/BlueskyClient/ViewModels/SignInPageViewModel.cs b/src/BlueskyClient/ViewModels/SignInPageViewModel.cs index 22e0b7b..d0fef03 100644 --- a/src/BlueskyClient/ViewModels/SignInPageViewModel.cs +++ b/src/BlueskyClient/ViewModels/SignInPageViewModel.cs @@ -43,6 +43,9 @@ public SignInPageViewModel( [ObservableProperty] private string _appPasswordInput = string.Empty; + [ObservableProperty] + private string _pdsInput = "bsky.social"; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(ErrorBannerVisible))] private string _signInErrorMessage = string.Empty; @@ -56,7 +59,7 @@ private async Task SignInAsync() _telemetry.TrackEvent(TelemetryConstants.SignInClicked); - Result result = await _authService.SignInAsync(UserHandleInput, AppPasswordInput); + Result result = await _authService.SignInAsync(UserHandleInput, AppPasswordInput, PdsInput.Contains("://") ? PdsInput : $"https://{PdsInput}"); SignInErrorMessage = result.IsSuccess ? string.Empty