Skip to content

Commit

Permalink
設定画面にMastodonアカウントの認証機能を実装
Browse files Browse the repository at this point in the history
  • Loading branch information
upsilon committed Mar 6, 2020
1 parent b90f3f2 commit b831ed9
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 6 deletions.
43 changes: 43 additions & 0 deletions OpenTween/Api/DataModel/MastodonAccessToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// OpenTween - Client of Twitter
// Copyright (c) 2017 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
// All rights reserved.
//
// This file is part of OpenTween.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.

#nullable enable annotations

using System.Runtime.Serialization;

namespace OpenTween.Api.DataModel
{
[DataContract]
public class MastodonAccessToken
{
[DataMember(Name = "access_token")]
public string AccessToken { get; set; }

[DataMember(Name = "token_type")]
public string TokenType { get; set; }

[DataMember(Name = "scope")]
public string Scope { get; set; }

[DataMember(Name = "created_at")]
public long CreatedAt { get; set; }
}
}
43 changes: 43 additions & 0 deletions OpenTween/Api/DataModel/MastodonRegisteredApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// OpenTween - Client of Twitter
// Copyright (c) 2017 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
// All rights reserved.
//
// This file is part of OpenTween.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.

#nullable enable annotations

using System.Runtime.Serialization;

namespace OpenTween.Api.DataModel
{
[DataContract]
public class MastodonRegisteredApp
{
[DataMember(Name = "id")]
public string Id { get; set; }

[DataMember(Name = "redirect_uri")]
public string RedirectUri { get; set; }

[DataMember(Name = "client_id")]
public string ClientId { get; set; }

[DataMember(Name = "client_secret")]
public string ClientSecret { get; set; }
}
}
79 changes: 79 additions & 0 deletions OpenTween/Api/MastodonApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,89 @@ namespace OpenTween.Api
public sealed class MastodonApi : IDisposable
{
public IMastodonApiConnection Connection { get; }
public Uri InstanceUri { get; }

public MastodonApi(Uri instanceUri)
: this(instanceUri, accessToken: null)
{
}

public MastodonApi(Uri instanceUri, string? accessToken)
{
this.Connection = new MastodonApiConnection(instanceUri, accessToken);
this.InstanceUri = instanceUri;
}

public Task<MastodonAccount> AccountsVerifyCredentials()
{
var endpoint = new Uri("/api/v1/accounts/verify_credentials", UriKind.Relative);

return this.Connection.GetAsync<MastodonAccount>(endpoint, null);
}

public async Task<MastodonRegisteredApp> AppsRegister(string clientName, Uri redirectUris,
string scopes, string? website = null)
{
var endpoint = new Uri("/api/v1/apps", UriKind.Relative);
var param = new Dictionary<string, string>
{
["client_name"] = clientName,
["redirect_uris"] = redirectUris.OriginalString,
["scopes"] = scopes,
};

if (website != null)
param["website"] = website;

var response = await this.Connection.PostLazyAsync<MastodonRegisteredApp>(endpoint, param)
.ConfigureAwait(false);

return await response.LoadJsonAsync()
.ConfigureAwait(false);
}

public Task<MastodonInstance> Instance()
{
var endpoint = new Uri("/api/v1/instance", UriKind.Relative);

return this.Connection.GetAsync<MastodonInstance>(endpoint, null);
}

public Uri OAuthAuthorize(string clientId, string responseType, Uri redirectUri, string scope)
{
var endpoint = new Uri("/oauth/authorize", UriKind.Relative);
var param = new Dictionary<string, string>
{
["client_id"] = clientId,
["response_type"] = responseType,
["redirect_uri"] = redirectUri.AbsoluteUri,
["scope"] = scope,
};

return new Uri(new Uri(this.InstanceUri, endpoint), "?" + MyCommon.BuildQueryString(param));
}

public async Task<MastodonAccessToken> OAuthToken(string clientId, string clientSecret, Uri redirectUri,
string grantType, string code, string? scope = null)
{
var endpoint = new Uri("/oauth/token", UriKind.Relative);
var param = new Dictionary<string, string>
{
["client_id"] = clientId,
["client_secret"] = clientSecret,
["redirect_uri"] = redirectUri.AbsoluteUri,
["grant_type"] = grantType,
["code"] = code,
};

if (scope != null)
param["scope"] = scope;

var response = await this.Connection.PostLazyAsync<MastodonAccessToken>(endpoint, param)
.ConfigureAwait(false);

return await response.LoadJsonAsync()
.ConfigureAwait(false);
}

public Task<MastodonStatus[]> TimelinesHome(long? maxId = null, long? sinceId = null, int? limit = null)
Expand Down
49 changes: 49 additions & 0 deletions OpenTween/Mastodon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,55 @@ public void Initialize(MastodonCredential account)
this.Username = account.Username;
}

public static async Task<MastodonRegisteredApp> RegisterClientAsync(Uri instanceUri)
{
using var api = new MastodonApi(instanceUri);
var redirectUri = new Uri("urn:ietf:wg:oauth:2.0:oob");
var scope = "read write follow";
var application = await api.AppsRegister(ApplicationSettings.ApplicationName, redirectUri, scope, ApplicationSettings.WebsiteUrl)
.ConfigureAwait(false);

System.Diagnostics.Debug.WriteLine($"ClientId: {application.ClientId}, ClientSecret: {application.ClientSecret}");

return application;
}

public static Uri GetAuthorizeUri(Uri instanceUri, string clientId)
{
var redirectUri = new Uri("urn:ietf:wg:oauth:2.0:oob");
var scope = "read write follow";
using var api = new MastodonApi(instanceUri);

return api.OAuthAuthorize(clientId, "code", redirectUri, scope);
}

public static async Task<string> GetAccessTokenAsync(Uri instanceUri, string clientId, string clientSecret,
string authorizationCode)
{
using var api = new MastodonApi(instanceUri);
var redirectUri = new Uri("urn:ietf:wg:oauth:2.0:oob");
var scope = "read write follow";
var token = await api.OAuthToken(clientId, clientSecret, redirectUri, "authorization_code", authorizationCode, scope)
.ConfigureAwait(false);

return token.AccessToken;
}

public static async Task<MastodonCredential> VerifyCredentialAsync(Uri instanceUri, string accessToken)
{
using var api = new MastodonApi(instanceUri, accessToken);
var account = await api.AccountsVerifyCredentials();
var instance = await api.Instance();

return new MastodonCredential
{
InstanceUri = instanceUri.AbsoluteUri,
UserId = account.Id,
Username = $"{account.Username}@{instance.Uri}",
AccessTokenPlain = accessToken,
};
}

public async Task<PostClass> PostStatusAsync(PostStatusParams param)
{
var response = await this.Api.StatusesPost(param.Text, param.InReplyToStatusId, param.MediaIds)
Expand Down
2 changes: 2 additions & 0 deletions OpenTween/OpenTween.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@
<Compile Include="Api\ApiLimit.cs" />
<Compile Include="Api\BitlyApi.cs" />
<Compile Include="Api\DataModel\GeoJson.cs" />
<Compile Include="Api\DataModel\MastodonAccessToken.cs" />
<Compile Include="Api\DataModel\MastodonAccount.cs" />
<Compile Include="Api\DataModel\MastodonApplication.cs" />
<Compile Include="Api\DataModel\MastodonAttachment.cs" />
<Compile Include="Api\DataModel\MastodonError.cs" />
<Compile Include="Api\DataModel\MastodonInstance.cs" />
<Compile Include="Api\DataModel\MastodonMention.cs" />
<Compile Include="Api\DataModel\MastodonRegisteredApp.cs" />
<Compile Include="Api\DataModel\MastodonStatus.cs" />
<Compile Include="Api\DataModel\MastodonTag.cs" />
<Compile Include="Api\DataModel\TwitterConfiguration.cs" />
Expand Down
60 changes: 58 additions & 2 deletions OpenTween/Setting/Panel/BasedPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ namespace OpenTween.Setting.Panel
{
public partial class BasedPanel : SettingPanelBase
{
private MastodonCredential? mastodonCredential = null;

public BasedPanel()
=> this.InitializeComponent();
{
this.InitializeComponent();
this.RefreshMastodonCredential();
}

public void LoadConfig(SettingCommon settingCommon)
{
Expand All @@ -56,6 +61,9 @@ public void LoadConfig(SettingCommon settingCommon)
if (primaryIndex != -1)
this.AuthUserCombo.SelectedIndex = primaryIndex;
}

this.mastodonCredential = settingCommon.MastodonPrimaryAccount;
this.RefreshMastodonCredential();
}

public void SaveConfig(SettingCommon settingCommon)
Expand All @@ -68,6 +76,25 @@ public void SaveConfig(SettingCommon settingCommon)
account.Primary = selectedIndex == index++;

settingCommon.UserAccounts = accounts;

var mastodonCredential = this.mastodonCredential;
if (mastodonCredential != null)
{
mastodonCredential.Primary = true;
settingCommon.MastodonAccounts = new[] { mastodonCredential };
}
else
{
settingCommon.MastodonAccounts = new MastodonCredential[0];
}
}

private void RefreshMastodonCredential()
{
if (mastodonCredential != null)
this.labelMastodonAccount.Text = this.mastodonCredential.Username;
else
this.labelMastodonAccount.Text = "(未設定)";
}

private void AuthClearButton_Click(object sender, EventArgs e)
Expand All @@ -86,8 +113,37 @@ private void AuthClearButton_Click(object sender, EventArgs e)
}
}

private void buttonMastodonAuth_Click(object sender, EventArgs e)
private async void buttonMastodonAuth_Click(object sender, EventArgs e)
{
var ret = InputDialog.Show(this, "インスタンスのURL (例: https://mstdn.jp/)", ApplicationSettings.ApplicationName, out var instanceUriStr);
if (ret != DialogResult.OK)
return;

if (!Uri.TryCreate(instanceUriStr, UriKind.Absolute, out var instanceUri))
return;

try
{
var application = await Mastodon.RegisterClientAsync(instanceUri);

var authorizeUri = Mastodon.GetAuthorizeUri(instanceUri, application.ClientId);

var code = AuthDialog.DoAuth(this, authorizeUri);
if (MyCommon.IsNullOrEmpty(code))
return;

var accessToken = await Mastodon.GetAccessTokenAsync(instanceUri, application.ClientId, application.ClientSecret, code);

this.mastodonCredential = await Mastodon.VerifyCredentialAsync(instanceUri, accessToken);

this.RefreshMastodonCredential();
}
catch (WebApiException ex)
{
var message = Properties.Resources.AuthorizeButton_Click2 + Environment.NewLine + ex.Message;
MessageBox.Show(this, message, "Authenticate", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
}
}
Loading

0 comments on commit b831ed9

Please sign in to comment.