From b831ed9c06bfc4ce6294f9d37bf6bc58770f544b Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Tue, 16 May 2017 21:37:43 +0900 Subject: [PATCH] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2=E3=81=ABMa?= =?UTF-8?q?stodon=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E6=A9=9F=E8=83=BD=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Api/DataModel/MastodonAccessToken.cs | 43 ++++++++++ .../Api/DataModel/MastodonRegisteredApp.cs | 43 ++++++++++ OpenTween/Api/MastodonApi.cs | 79 +++++++++++++++++++ OpenTween/Mastodon.cs | 49 ++++++++++++ OpenTween/OpenTween.csproj | 2 + OpenTween/Setting/Panel/BasedPanel.cs | 60 +++++++++++++- OpenTween/Tween.cs | 31 +++++++- 7 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 OpenTween/Api/DataModel/MastodonAccessToken.cs create mode 100644 OpenTween/Api/DataModel/MastodonRegisteredApp.cs diff --git a/OpenTween/Api/DataModel/MastodonAccessToken.cs b/OpenTween/Api/DataModel/MastodonAccessToken.cs new file mode 100644 index 000000000..e2317a861 --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonAccessToken.cs @@ -0,0 +1,43 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2017 kim_upsilon (@kim_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 , 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; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonRegisteredApp.cs b/OpenTween/Api/DataModel/MastodonRegisteredApp.cs new file mode 100644 index 000000000..51f21d18d --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonRegisteredApp.cs @@ -0,0 +1,43 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2017 kim_upsilon (@kim_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 , 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; } + } +} diff --git a/OpenTween/Api/MastodonApi.cs b/OpenTween/Api/MastodonApi.cs index c8f07e0c6..04b0b208a 100644 --- a/OpenTween/Api/MastodonApi.cs +++ b/OpenTween/Api/MastodonApi.cs @@ -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 AccountsVerifyCredentials() + { + var endpoint = new Uri("/api/v1/accounts/verify_credentials", UriKind.Relative); + + return this.Connection.GetAsync(endpoint, null); + } + + public async Task AppsRegister(string clientName, Uri redirectUris, + string scopes, string? website = null) + { + var endpoint = new Uri("/api/v1/apps", UriKind.Relative); + var param = new Dictionary + { + ["client_name"] = clientName, + ["redirect_uris"] = redirectUris.OriginalString, + ["scopes"] = scopes, + }; + + if (website != null) + param["website"] = website; + + var response = await this.Connection.PostLazyAsync(endpoint, param) + .ConfigureAwait(false); + + return await response.LoadJsonAsync() + .ConfigureAwait(false); + } + + public Task Instance() + { + var endpoint = new Uri("/api/v1/instance", UriKind.Relative); + + return this.Connection.GetAsync(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 + { + ["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 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 + { + ["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(endpoint, param) + .ConfigureAwait(false); + + return await response.LoadJsonAsync() + .ConfigureAwait(false); } public Task TimelinesHome(long? maxId = null, long? sinceId = null, int? limit = null) diff --git a/OpenTween/Mastodon.cs b/OpenTween/Mastodon.cs index f3dee230a..daabc1335 100644 --- a/OpenTween/Mastodon.cs +++ b/OpenTween/Mastodon.cs @@ -50,6 +50,55 @@ public void Initialize(MastodonCredential account) this.Username = account.Username; } + public static async Task 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 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 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 PostStatusAsync(PostStatusParams param) { var response = await this.Api.StatusesPost(param.Text, param.InReplyToStatusId, param.MediaIds) diff --git a/OpenTween/OpenTween.csproj b/OpenTween/OpenTween.csproj index de10743e6..7b6fb14fa 100644 --- a/OpenTween/OpenTween.csproj +++ b/OpenTween/OpenTween.csproj @@ -69,12 +69,14 @@ + + diff --git a/OpenTween/Setting/Panel/BasedPanel.cs b/OpenTween/Setting/Panel/BasedPanel.cs index eed2e685f..a86f250c8 100644 --- a/OpenTween/Setting/Panel/BasedPanel.cs +++ b/OpenTween/Setting/Panel/BasedPanel.cs @@ -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) { @@ -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) @@ -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) @@ -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; + } } } } diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index f89d4831e..af9a09b4a 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -142,7 +142,7 @@ public partial class TweenMain : OTBaseForm private readonly TwitterApi twitterApi = new TwitterApi(); private Twitter tw = null!; - private Mastodon mastodon; + private Mastodon mastodon = new Mastodon(); //Growl呼び出し部 private readonly GrowlHelper gh = new GrowlHelper(ApplicationSettings.ApplicationName); @@ -1160,15 +1160,14 @@ private void TweenMain_Load(object sender, EventArgs e) if (this._statuses.MuteTab == null) this._statuses.AddTab(new MuteTabModel()); - if (this.mastodon != null) - this._statuses.AddTab(new MastodonHomeTab(this.mastodon, "Mastodon")); - foreach (var tab in _statuses.Tabs) { if (!AddNewTab(tab, startup: true)) throw new TabException(Properties.Resources.TweenMain_LoadText1); } + this.ReloadMastodonHomeTab(startup: true); + this._statuses.SelectTab(this.ListTab.SelectedTab.Text); // タブの位置を調整する @@ -3516,10 +3515,24 @@ private DialogResult ShowSettingDialog(bool showTaskbarIcon = false) return result; } + private void ReloadMastodonHomeTab(bool startup = false) + { + var currentTab = this._statuses.GetTabByType(); + if (currentTab != null) + this.RemoveSpecifiedTab(currentTab.TabName, false); + + var tabName = currentTab?.TabName ?? "Mastodon"; + + var newTab = new MastodonHomeTab(this.mastodon, tabName); + this._statuses.AddTab(newTab); + this.AddNewTab(newTab, startup); + } + private async void SettingStripMenuItem_Click(object sender, EventArgs e) { // 設定画面表示前のユーザー情報 var oldUser = new { tw.AccessToken, tw.AccessTokenSecret, tw.Username, tw.UserId }; + var oldMastodonUser = SettingManager.Common.MastodonPrimaryAccount; var oldIconSz = SettingManager.Common.IconSize; @@ -3538,6 +3551,13 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) this.tw.Initialize("", "", "", 0); } + var primaryMastodonAccount = SettingManager.Common.MastodonPrimaryAccount; + if (primaryMastodonAccount != null && primaryMastodonAccount != oldMastodonUser) + { + this.mastodon.Initialize(primaryMastodonAccount); + this.ReloadMastodonHomeTab(); + } + tw.RestrictFavCheck = SettingManager.Common.RestrictFavCheck; tw.ReadOwnPost = SettingManager.Common.ReadOwnPost; ShortUrl.Instance.DisableExpanding = !SettingManager.Common.TinyUrlResolve; @@ -3771,6 +3791,9 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) if (tw.UserId != oldUser.UserId) await this.doGetFollowersMenu(); + + if (SettingManager.Common.MastodonPrimaryAccount != oldMastodonUser) + await this.RefreshTabAsync(); } ///