From 8e748514e713578bc2f6f7a18bf1c669803216cd Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 30 Apr 2017 05:31:07 +0900 Subject: [PATCH 01/25] =?UTF-8?q?UserAccount=E3=82=AF=E3=83=A9=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E3=83=97=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3=E5=90=8D?= =?UTF-8?q?=E7=AD=89=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/AppendSettingDialog.cs | 4 +- OpenTween/Setting/Panel/BasedPanel.cs | 4 +- OpenTween/Setting/SettingCommon.cs | 54 ++++++--------------------- OpenTween/Setting/SettingManager.cs | 4 +- 4 files changed, 17 insertions(+), 49 deletions(-) diff --git a/OpenTween/AppendSettingDialog.cs b/OpenTween/AppendSettingDialog.cs index 1c644813e..b2d79f0c5 100644 --- a/OpenTween/AppendSettingDialog.cs +++ b/OpenTween/AppendSettingDialog.cs @@ -263,8 +263,8 @@ public void ApplyNetworkSettings() { Username = accessTokenResponse["screen_name"], UserId = long.Parse(accessTokenResponse["user_id"]), - Token = accessTokenResponse["oauth_token"], - TokenSecret = accessTokenResponse["oauth_token_secret"], + AccessToken = accessTokenResponse["oauth_token"], + AccessSecretPlain = accessTokenResponse["oauth_token_secret"], }; } diff --git a/OpenTween/Setting/Panel/BasedPanel.cs b/OpenTween/Setting/Panel/BasedPanel.cs index 95911f992..7f7f79d83 100644 --- a/OpenTween/Setting/Panel/BasedPanel.cs +++ b/OpenTween/Setting/Panel/BasedPanel.cs @@ -67,8 +67,8 @@ public void SaveConfig(SettingCommon settingCommon) var selectedAccount = accounts[selectedIndex]; settingCommon.UserId = selectedAccount.UserId; settingCommon.UserName = selectedAccount.Username; - settingCommon.Token = selectedAccount.Token; - settingCommon.TokenSecret = selectedAccount.TokenSecret; + settingCommon.Token = selectedAccount.AccessToken; + settingCommon.TokenSecret = selectedAccount.AccessSecretPlain; } else { diff --git a/OpenTween/Setting/SettingCommon.cs b/OpenTween/Setting/SettingCommon.cs index a3131e527..13ea7e2fc 100644 --- a/OpenTween/Setting/SettingCommon.cs +++ b/OpenTween/Setting/SettingCommon.cs @@ -329,53 +329,21 @@ public void Validate() public class UserAccount { - public string Username = ""; - public long UserId = 0; - public string Token = ""; - [XmlIgnore] - public string TokenSecret = ""; + public string Username { get; set; } = ""; - public string EncryptTokenSecret - { - get => this.Encrypt(this.TokenSecret); - set => this.TokenSecret = this.Decrypt(value); - } + public long UserId { get; set; } = 0; - private string Encrypt(string password) - { - if (MyCommon.IsNullOrEmpty(password)) password = ""; - if (password.Length > 0) - { - try - { - return MyCommon.EncryptString(password); - } - catch (Exception) - { - return ""; - } - } - else - { - return ""; - } - } + [XmlElement("Token")] + public string AccessToken { get; set; } = ""; - private string Decrypt(string password) + [XmlElement("EncryptTokenSecret")] + public string AccessSecretEncrypted { get; set; } = ""; + + [XmlIgnore] + public string AccessSecretPlain { - if (MyCommon.IsNullOrEmpty(password)) password = ""; - if (password.Length > 0) - { - try - { - password = MyCommon.DecryptString(password); - } - catch (Exception) - { - password = ""; - } - } - return password; + get => MyCommon.IsNullOrEmpty(this.AccessSecretEncrypted) ? "" : MyCommon.DecryptString(this.AccessSecretEncrypted); + set => this.AccessSecretEncrypted = MyCommon.IsNullOrEmpty(value) ? "" : MyCommon.EncryptString(value); } public override string ToString() diff --git a/OpenTween/Setting/SettingManager.cs b/OpenTween/Setting/SettingManager.cs index 44babc89b..90696c186 100644 --- a/OpenTween/Setting/SettingManager.cs +++ b/OpenTween/Setting/SettingManager.cs @@ -79,8 +79,8 @@ public void LoadCommon() { Username = settings.UserName, UserId = settings.UserId, - Token = settings.Token, - TokenSecret = settings.TokenSecret, + AccessToken = settings.Token, + AccessSecretPlain = settings.TokenSecret, }; settings.UserAccounts.Add(account); From 4cd9dd5e78ee91213753739a806fe78ae461bb6b Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Thu, 4 May 2017 22:02:35 +0900 Subject: [PATCH 02/25] =?UTF-8?q?SettingCommon=E3=82=AF=E3=83=A9=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E3=83=97=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3=E3=82=92?= =?UTF-8?q?=E6=95=B4=E7=90=86,=20=E4=BD=BF=E7=94=A8=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84Password=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=83=89=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Setting/SettingCommon.cs | 55 +++--------------------------- 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/OpenTween/Setting/SettingCommon.cs b/OpenTween/Setting/SettingCommon.cs index 13ea7e2fc..8b41a157e 100644 --- a/OpenTween/Setting/SettingCommon.cs +++ b/OpenTween/Setting/SettingCommon.cs @@ -46,65 +46,20 @@ public void Save(string settingsPath) #endregion public List UserAccounts = new(); - public string UserName = ""; - - [XmlIgnore] - public string Password = ""; - - public string EncryptPassword - { - get => this.Encrypt(this.Password); - set => this.Password = this.Decrypt(value); - } + public long UserId = 0; + public string UserName = ""; public string Token = ""; + [XmlIgnore] public string TokenSecret = ""; public string EncryptTokenSecret { - get => this.Encrypt(this.TokenSecret); - set => this.TokenSecret = this.Decrypt(value); - } - - private string Encrypt(string password) - { - if (MyCommon.IsNullOrEmpty(password)) password = ""; - if (password.Length > 0) - { - try - { - return MyCommon.EncryptString(password); - } - catch (Exception) - { - return ""; - } - } - else - { - return ""; - } + get => string.IsNullOrEmpty(this.TokenSecret) ? "" : MyCommon.EncryptString(this.TokenSecret); + set => this.TokenSecret = string.IsNullOrEmpty(value) ? "" : MyCommon.DecryptString(value); } - private string Decrypt(string password) - { - if (MyCommon.IsNullOrEmpty(password)) password = ""; - if (password.Length > 0) - { - try - { - password = MyCommon.DecryptString(password); - } - catch (Exception) - { - password = ""; - } - } - return password; - } - - public long UserId = 0; public List TabList = new(); public int TimelinePeriod = 90; public int ReplyPeriod = 180; From 0193a5b39f483d4f1a3788e70cbbdaba5a8d7032 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Thu, 4 May 2017 22:04:57 +0900 Subject: [PATCH 03/25] =?UTF-8?q?=E9=81=B8=E6=8A=9E=E4=B8=AD=E3=81=AE?= =?UTF-8?q?=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E3=82=92=20UserAc?= =?UTF-8?q?count.Primary=20=E3=83=97=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=81=AB=E3=82=88=E3=81=A3=E3=81=A6=E7=AE=A1=E7=90=86=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SettingCommon (UserAccount ではない) クラス内の UserId, UserName, Token, TokenSecret の各プロパティは、複数アカウントが登録されている場合における 現在使用中のアカウントを表す役割を担っていた。 しかし、この方法では今後 UserAccount クラスにプロパティが追加される際に SettingCommon クラスにも同名のプロパティを追加する必要があるなど不都合が 生じるため、新たに UserAccount.Primary プロパティを追加することで管理を 行うように変更した。 --- OpenTween/Setting/Panel/BasedPanel.cs | 15 ++++++++---- OpenTween/Setting/SettingCommon.cs | 34 ++++++++++++++++++++++++--- OpenTween/Setting/SettingManager.cs | 2 +- OpenTween/Tween.cs | 14 ++++++++--- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/OpenTween/Setting/Panel/BasedPanel.cs b/OpenTween/Setting/Panel/BasedPanel.cs index 7f7f79d83..f5ba44543 100644 --- a/OpenTween/Setting/Panel/BasedPanel.cs +++ b/OpenTween/Setting/Panel/BasedPanel.cs @@ -34,6 +34,7 @@ using System.Linq; using System.Text; using System.Windows.Forms; +using OpenTween; namespace OpenTween.Setting.Panel { @@ -44,15 +45,16 @@ public BasedPanel() public void LoadConfig(SettingCommon settingCommon) { + var accounts = settingCommon.UserAccounts; + using (ControlTransaction.Update(this.AuthUserCombo)) { this.AuthUserCombo.Items.Clear(); - this.AuthUserCombo.Items.AddRange(settingCommon.UserAccounts.ToArray()); + this.AuthUserCombo.Items.AddRange(accounts.ToArray()); - var selectedUserId = settingCommon.UserId; - var selectedAccount = settingCommon.UserAccounts.FirstOrDefault(x => x.UserId == selectedUserId); - if (selectedAccount != null) - this.AuthUserCombo.SelectedItem = selectedAccount; + var primaryIndex = accounts.FindIndex(x => x.Primary); + if (primaryIndex != -1) + this.AuthUserCombo.SelectedIndex = primaryIndex; } } @@ -64,6 +66,9 @@ public void SaveConfig(SettingCommon settingCommon) var selectedIndex = this.AuthUserCombo.SelectedIndex; if (selectedIndex != -1) { + foreach (var (account, index) in accounts.WithIndex()) + account.Primary = selectedIndex == index; + var selectedAccount = accounts[selectedIndex]; settingCommon.UserId = selectedAccount.UserId; settingCommon.UserName = selectedAccount.Username; diff --git a/OpenTween/Setting/SettingCommon.cs b/OpenTween/Setting/SettingCommon.cs index 8b41a157e..f3a891ef7 100644 --- a/OpenTween/Setting/SettingCommon.cs +++ b/OpenTween/Setting/SettingCommon.cs @@ -28,6 +28,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Windows.Forms; using System.Xml.Serialization; using OpenTween.Thumbnail; @@ -39,13 +40,38 @@ public class SettingCommon : SettingBase { #region "Settingクラス基本" public static SettingCommon Load(string settingsPath) - => LoadSettings(settingsPath); + { + var settings = LoadSettings(settingsPath); + + // UserAccount.Primary が追加される前との互換性を保つ + var accounts = settings.UserAccounts; + if (accounts.Count != 0 && settings.PrimaryAccount == null) + { + var primaryAccount = accounts.FirstOrDefault(x => x.UserId == settings.UserId) ?? accounts.First(); + primaryAccount.Primary = true; + } + + return settings; + } public void Save(string settingsPath) - => SaveSettings(this, settingsPath); + { + var primaryAccount = this.PrimaryAccount; + + // UserAccount.Primary が追加される前との互換性を保つ + this.UserId = primaryAccount?.UserId ?? 0; + this.UserName = primaryAccount?.Username ?? ""; + this.Token = primaryAccount?.AccessToken ?? ""; + this.TokenSecret = primaryAccount?.AccessSecretPlain ?? ""; + + SaveSettings(this, settingsPath); + } #endregion - public List UserAccounts = new(); + public List UserAccounts { get; set; } = new(); + + [XmlIgnore] + public UserAccount PrimaryAccount => this.UserAccounts.FirstOrDefault(x => x.Primary); public long UserId = 0; public string UserName = ""; @@ -284,6 +310,8 @@ public void Validate() public class UserAccount { + public bool Primary { get; set; } + public string Username { get; set; } = ""; public long UserId { get; set; } = 0; diff --git a/OpenTween/Setting/SettingManager.cs b/OpenTween/Setting/SettingManager.cs index 90696c186..431ab6a3b 100644 --- a/OpenTween/Setting/SettingManager.cs +++ b/OpenTween/Setting/SettingManager.cs @@ -49,7 +49,7 @@ public class SettingManager /// ユーザによる設定が必要な項目が残っているか public bool IsIncomplete - => MyCommon.IsNullOrEmpty(this.Common.UserName); + => this.Common.PrimaryAccount == null; public bool IsFirstRun { get; private set; } = false; diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 5cc1ace7e..26fa6d167 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -522,7 +522,8 @@ ThumbnailGenerator thumbGenerator var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); // 認証関連 - this.tw.Initialize(this.settings.Common.Token, this.settings.Common.TokenSecret, this.settings.Common.UserName, this.settings.Common.UserId); + var primaryAccount = this.settings.Common.PrimaryAccount; + this.tw.Initialize(primaryAccount.AccessToken, primaryAccount.AccessSecretPlain, primaryAccount.Username, primaryAccount.UserId); this.initial = true; @@ -2576,10 +2577,17 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) { this.settings.ApplySettings(); - if (MyCommon.IsNullOrEmpty(this.settings.Common.Token)) + var primaryAccount = this.settings.Common.PrimaryAccount; + if (primaryAccount != null) + { + this.tw.Initialize(primaryAccount.AccessToken, primaryAccount.AccessSecretPlain, primaryAccount.Username, primaryAccount.UserId); + } + else + { this.tw.ClearAuthInfo(); + this.tw.Initialize("", "", "", 0); + } - this.tw.Initialize(this.settings.Common.Token, this.settings.Common.TokenSecret, this.settings.Common.UserName, this.settings.Common.UserId); this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; From 0be2f72ede28ed0b0a0f4060d6f2520af362499d Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 6 May 2017 07:13:55 +0900 Subject: [PATCH 04/25] =?UTF-8?q?IsDistributableTabType,IsInnerStorageTabT?= =?UTF-8?q?ype=20=E3=82=92TabModel=E3=81=AE=E5=9E=8B=E3=81=AB=E5=9F=BA?= =?UTF-8?q?=E3=81=A5=E3=81=84=E3=81=A6=E5=88=A4=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/TabModelTest.cs | 62 +++++++++++++++----------- OpenTween/Models/TabModel.cs | 13 +++++- OpenTween/Models/TabUsageTypeExt.cs | 25 ----------- 3 files changed, 47 insertions(+), 53 deletions(-) diff --git a/OpenTween.Tests/Models/TabModelTest.cs b/OpenTween.Tests/Models/TabModelTest.cs index 2cf139d1a..a8c7e2076 100644 --- a/OpenTween.Tests/Models/TabModelTest.cs +++ b/OpenTween.Tests/Models/TabModelTest.cs @@ -729,6 +729,42 @@ public void GetterSlice_ErrorTest() Assert.Throws(() => tab[2, 0]); // 範囲内だが startIndex > endIndex } + + public static TheoryData IsDistributableTabTypeFixtures = new TheoryData + { + { false, new HomeTabModel("Recent") }, + { true, new MentionsTabModel("Reply") }, + { false, new DirectMessagesTabModel("Direct") }, + { false, new FavoritesTabModel("Favorite") }, + { true, new FilterTabModel("Filter") }, + { false, new ListTimelineTabModel("List", new ListElement()) }, + { false, new UserTimelineTabModel("User", "twitterapi") }, + { false, new PublicSearchTabModel("Search") }, + { false, new RelatedPostsTabModel("Related", new PostClass()) }, + }; + + [Theory] + [MemberData(nameof(IsDistributableTabTypeFixtures))] + public void IsDistributableTabType_Test(bool expected, TabModel tab) + => Assert.Equal(expected, tab.IsDistributableTabType); + + public static TheoryData IsInnerStorageTabTypeFixtures = new TheoryData + { + { false, new HomeTabModel("Recent") }, + { false, new MentionsTabModel("Reply") }, + { true, new DirectMessagesTabModel("Direct") }, + { false, new FavoritesTabModel("Favorite") }, + { false, new FilterTabModel("Filter") }, + { true, new ListTimelineTabModel("List", new ListElement()) }, + { true, new UserTimelineTabModel("User", "twitterapi") }, + { true, new PublicSearchTabModel("Search") }, + { true, new RelatedPostsTabModel("Related", new PostClass()) }, + }; + + [Theory] + [MemberData(nameof(IsInnerStorageTabTypeFixtures))] + public void IsInnerStorageTabType_Test(bool expected, TabModel tab) + => Assert.Equal(expected, tab.IsInnerStorageTabType); } public class TabUsageTypeExtTest @@ -745,31 +781,5 @@ public class TabUsageTypeExtTest [InlineData(MyCommon.TabUsageType.Related, false)] public void IsDefault_Test(MyCommon.TabUsageType tabType, bool expected) => Assert.Equal(expected, tabType.IsDefault()); - - [Theory] - [InlineData(MyCommon.TabUsageType.Home, false)] - [InlineData(MyCommon.TabUsageType.Mentions, true)] - [InlineData(MyCommon.TabUsageType.DirectMessage, false)] - [InlineData(MyCommon.TabUsageType.Favorites, false)] - [InlineData(MyCommon.TabUsageType.UserDefined, true)] - [InlineData(MyCommon.TabUsageType.Lists, false)] - [InlineData(MyCommon.TabUsageType.UserTimeline, false)] - [InlineData(MyCommon.TabUsageType.PublicSearch, false)] - [InlineData(MyCommon.TabUsageType.Related, false)] - public void IsDistributable_Test(MyCommon.TabUsageType tabType, bool expected) - => Assert.Equal(expected, tabType.IsDistributable()); - - [Theory] - [InlineData(MyCommon.TabUsageType.Home, false)] - [InlineData(MyCommon.TabUsageType.Mentions, false)] - [InlineData(MyCommon.TabUsageType.DirectMessage, true)] - [InlineData(MyCommon.TabUsageType.Favorites, false)] - [InlineData(MyCommon.TabUsageType.UserDefined, false)] - [InlineData(MyCommon.TabUsageType.Lists, true)] - [InlineData(MyCommon.TabUsageType.UserTimeline, true)] - [InlineData(MyCommon.TabUsageType.PublicSearch, true)] - [InlineData(MyCommon.TabUsageType.Related, true)] - public void IsInnerStorage_Test(MyCommon.TabUsageType tabType, bool expected) - => Assert.Equal(expected, tabType.IsInnerStorage()); } } diff --git a/OpenTween/Models/TabModel.cs b/OpenTween/Models/TabModel.cs index d9c054279..5d884896b 100644 --- a/OpenTween/Models/TabModel.cs +++ b/OpenTween/Models/TabModel.cs @@ -68,11 +68,20 @@ public virtual ConcurrentDictionary Posts public long[] StatusIds => this.ids.ToArray(); + /// + /// デフォルトタブかどうかを示す値を取得します。 + /// public bool IsDefaultTabType => this.TabType.IsDefault(); - public bool IsDistributableTabType => this.TabType.IsDistributable(); + /// + /// 振り分け可能タブかどうかを示す値を取得します。 + /// + public bool IsDistributableTabType => this is FilterTabModel; - public bool IsInnerStorageTabType => this.TabType.IsInnerStorage(); + /// + /// 内部ストレージを使用するタブかどうかを示す値を取得します。 + /// + public bool IsInnerStorageTabType => this is InternalStorageTabModel; /// /// 次回起動時にも保持されるタブか(SettingTabsに保存されるか) diff --git a/OpenTween/Models/TabUsageTypeExt.cs b/OpenTween/Models/TabUsageTypeExt.cs index 3f85fe44a..c038d963b 100644 --- a/OpenTween/Models/TabUsageTypeExt.cs +++ b/OpenTween/Models/TabUsageTypeExt.cs @@ -41,35 +41,10 @@ public static class TabUsageTypeExt MyCommon.TabUsageType.Favorites | MyCommon.TabUsageType.Mute; - private const MyCommon.TabUsageType DistributableTabTypeMask = - MyCommon.TabUsageType.Mentions | - MyCommon.TabUsageType.UserDefined | - MyCommon.TabUsageType.Mute; - - private const MyCommon.TabUsageType InnerStorageTabTypeMask = - MyCommon.TabUsageType.DirectMessage | - MyCommon.TabUsageType.PublicSearch | - MyCommon.TabUsageType.Lists | - MyCommon.TabUsageType.UserTimeline | - MyCommon.TabUsageType.Related | - MyCommon.TabUsageType.SearchResults; - /// /// デフォルトタブかどうかを示す値を取得します。 /// public static bool IsDefault(this MyCommon.TabUsageType tabType) => (tabType & DefaultTabTypeMask) != 0; - - /// - /// 振り分け可能タブかどうかを示す値を取得します。 - /// - public static bool IsDistributable(this MyCommon.TabUsageType tabType) - => (tabType & DistributableTabTypeMask) != 0; - - /// - /// 内部ストレージを使用するタブかどうかを示す値を取得します。 - /// - public static bool IsInnerStorage(this MyCommon.TabUsageType tabType) - => (tabType & InnerStorageTabTypeMask) != 0; } } From e048c6329265b2bb1abe838174e4e3041446a920 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 8 May 2017 22:55:28 +0900 Subject: [PATCH 05/25] =?UTF-8?q?=E3=83=84=E3=82=A4=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=81=B5=E3=81=81=E3=81=BC=E3=83=BBRT=E3=83=BB?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=AE=E5=87=A6=E7=90=86=E3=82=92PostClass?= =?UTF-8?q?=E6=B4=BE=E7=94=9F=E3=82=AF=E3=83=A9=E3=82=B9=E3=81=AE=E3=83=A1?= =?UTF-8?q?=E3=82=BD=E3=83=83=E3=83=89=E3=81=A8=E3=81=97=E3=81=A6=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/TwitterPostFactoryTest.cs | 49 +++++-- OpenTween/Models/PostClass.cs | 12 ++ OpenTween/Models/TwitterDmPost.cs | 39 ++++++ OpenTween/Models/TwitterPostFactory.cs | 6 +- OpenTween/Models/TwitterStatusPost.cs | 120 ++++++++++++++++++ OpenTween/Tween.cs | 92 ++------------ OpenTween/Twitter.cs | 4 +- 7 files changed, 228 insertions(+), 94 deletions(-) create mode 100644 OpenTween/Models/TwitterDmPost.cs create mode 100644 OpenTween/Models/TwitterStatusPost.cs diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index a62d8a347..8c589a239 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; +using OpenTween.Api; using OpenTween.Api.DataModel; using Xunit; @@ -75,9 +76,11 @@ private TwitterUser CreateUser() [Fact] public void CreateFromStatus_Test() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); - var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 20000L, followerIds: EmptyIdSet); Assert.Equal(status.Id, post.StatusId); Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); @@ -122,10 +125,12 @@ public void CreateFromStatus_Test() [Fact] public void CreateFromStatus_AuthorTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); var selfUserId = status.User.Id; - var post = factory.CreateFromStatus(status, selfUserId, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, status, selfUserId, followerIds: EmptyIdSet); Assert.True(post.IsMe); } @@ -133,10 +138,12 @@ public void CreateFromStatus_AuthorTest() [Fact] public void CreateFromStatus_FollowerTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); var followerIds = new HashSet { status.User.Id }; - var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 20000L, followerIds); Assert.False(post.IsOwl); } @@ -144,10 +151,12 @@ public void CreateFromStatus_FollowerTest() [Fact] public void CreateFromStatus_NotFollowerTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); var followerIds = new HashSet { 30000L }; - var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 20000L, followerIds); Assert.True(post.IsOwl); } @@ -155,6 +164,8 @@ public void CreateFromStatus_NotFollowerTest() [Fact] public void CreateFromStatus_RetweetTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var originalStatus = this.CreateStatus(); @@ -162,7 +173,7 @@ public void CreateFromStatus_RetweetTest() retweetStatus.RetweetedStatus = originalStatus; retweetStatus.Source = "Twitter Web App"; - var post = factory.CreateFromStatus(retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet); Assert.Equal(retweetStatus.Id, post.StatusId); Assert.Equal(retweetStatus.User.Id, post.RetweetedByUserId); @@ -215,6 +226,8 @@ private TwitterMessageEvent CreateDirectMessage(string senderId, string recipien [Fact] public void CreateFromDirectMessageEvent_Test() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var selfUser = this.CreateUser(); @@ -226,7 +239,7 @@ public void CreateFromDirectMessageEvent_Test() [otherUser.IdStr] = otherUser, }; var apps = this.CreateApps(); - var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id); + var post = factory.CreateFromDirectMessageEvent(twitter, eventItem, users, apps, selfUserId: selfUser.Id); Assert.Equal(long.Parse(eventItem.Id), post.StatusId); Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); @@ -271,6 +284,8 @@ public void CreateFromDirectMessageEvent_Test() [Fact] public void CreateFromDirectMessageEvent_SenderTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var selfUser = this.CreateUser(); @@ -282,7 +297,7 @@ public void CreateFromDirectMessageEvent_SenderTest() [otherUser.IdStr] = otherUser, }; var apps = this.CreateApps(); - var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id); + var post = factory.CreateFromDirectMessageEvent(twitter, eventItem, users, apps, selfUserId: selfUser.Id); Assert.Equal(otherUser.Id, post.UserId); Assert.False(post.IsOwl); @@ -292,6 +307,8 @@ public void CreateFromDirectMessageEvent_SenderTest() [Fact] public void CreateFromStatus_MediaAltTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); @@ -311,7 +328,7 @@ public void CreateFromStatus_MediaAltTest() }, }; - var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = string.Format(Properties.Resources.ImageAltText, "代替テキスト"); Assert.Equal(accessibleText, post.AccessibleText); @@ -323,6 +340,8 @@ public void CreateFromStatus_MediaAltTest() [Fact] public void CreateFromStatus_MediaNoAltTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); @@ -342,7 +361,7 @@ public void CreateFromStatus_MediaNoAltTest() }, }; - var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 100L, followerIds: EmptyIdSet); Assert.Equal("pic.twitter.com/hoge", post.AccessibleText); Assert.Equal("pic.twitter.com/hoge", post.Text); @@ -353,6 +372,8 @@ public void CreateFromStatus_MediaNoAltTest() [Fact] public void CreateFromStatus_QuotedUrlTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); @@ -383,7 +404,7 @@ public void CreateFromStatus_QuotedUrlTest() FullText = "test", }; - var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); Assert.Equal(accessibleText, post.AccessibleText); @@ -395,6 +416,8 @@ public void CreateFromStatus_QuotedUrlTest() [Fact] public void CreateFromStatus_QuotedUrlWithPermelinkTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); @@ -418,7 +441,7 @@ public void CreateFromStatus_QuotedUrlWithPermelinkTest() Expanded = "https://twitter.com/hoge/status/1234567890", }; - var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); Assert.Equal(accessibleText, post.AccessibleText); @@ -430,6 +453,8 @@ public void CreateFromStatus_QuotedUrlWithPermelinkTest() [Fact] public void CreateFromStatus_QuotedUrlNoReferenceTest() { + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitter = new Twitter(twitterApi); var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); @@ -449,7 +474,7 @@ public void CreateFromStatus_QuotedUrlNoReferenceTest() }; status.QuotedStatus = null; - var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + var post = factory.CreateFromStatus(twitter, status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = "twitter.com/hoge/status/1…"; Assert.Equal(accessibleText, post.AccessibleText); diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index e52e333f7..0a1f3a9e5 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -436,6 +436,18 @@ private string ReplaceToExpandedUrl(string html, out bool completedAll) return html; } + public virtual Task FavoriteAsync(SettingCommon settingCommon) + => Task.CompletedTask; + + public virtual Task UnfavoriteAsync() + => Task.CompletedTask; + + public virtual Task RetweetAsync(SettingCommon settingCommon) + => throw new NotImplementedException(); + + public virtual Task DeleteAsync() + => Task.CompletedTask; + public PostClass Clone() { var clone = (PostClass)this.MemberwiseClone(); diff --git a/OpenTween/Models/TwitterDmPost.cs b/OpenTween/Models/TwitterDmPost.cs new file mode 100644 index 000000000..a6443d07d --- /dev/null +++ b/OpenTween/Models/TwitterDmPost.cs @@ -0,0 +1,39 @@ +// 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 + +using System.Globalization; +using System.Threading.Tasks; + +namespace OpenTween.Models +{ + public class TwitterDmPost : PostClass + { + private readonly Twitter twitter; + + public TwitterDmPost(Twitter twitter) + => this.twitter = twitter; + + public override Task DeleteAsync() + => this.twitter.Api.DirectMessagesEventsDestroy(this.StatusId.ToString(CultureInfo.InvariantCulture)); + } +} diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index b792b4bda..eeb502971 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -52,13 +52,14 @@ public string[] GetReceivedHashtags() } public PostClass CreateFromStatus( + Twitter twitter, TwitterStatus status, long selfUserId, ISet followerIds, bool favTweet = false ) { - var post = new PostClass(); + var post = new TwitterStatusPost(twitter); TwitterEntities entities; string sourceHtml; @@ -229,13 +230,14 @@ public PostClass CreateFromStatus( } public PostClass CreateFromDirectMessageEvent( + Twitter twitter, TwitterMessageEvent eventItem, IReadOnlyDictionary users, IReadOnlyDictionary apps, long selfUserId ) { - var post = new PostClass(); + var post = new TwitterDmPost(twitter); post.StatusId = long.Parse(eventItem.Id); var timestamp = long.Parse(eventItem.CreatedTimestamp); diff --git a/OpenTween/Models/TwitterStatusPost.cs b/OpenTween/Models/TwitterStatusPost.cs new file mode 100644 index 000000000..d217c1425 --- /dev/null +++ b/OpenTween/Models/TwitterStatusPost.cs @@ -0,0 +1,120 @@ +// 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 + +using System; +using System.Linq; +using System.Threading.Tasks; +using OpenTween.Api; +using OpenTween.Api.DataModel; +using OpenTween.Connection; +using OpenTween.Setting; + +namespace OpenTween.Models +{ + public class TwitterStatusPost : PostClass + { + private readonly Twitter twitter; + + public TwitterStatusPost(Twitter twitter) + => this.twitter = twitter; + + public override async Task FavoriteAsync(SettingCommon settingCommon) + { + var statusId = this.RetweetedId ?? this.StatusId; + + try + { + await this.twitter.Api.FavoritesCreate(statusId) + .IgnoreResponse() + .ConfigureAwait(false); + } + catch (TwitterApiException ex) + when (ex.Errors.All(x => x.Code == TwitterErrorCode.AlreadyFavorited)) + { + // エラーコード 139 のみの場合は成功と見なす + } + + if (settingCommon.RestrictFavCheck) + { + var status = await this.twitter.Api.StatusesShow(statusId) + .ConfigureAwait(false); + + if (status.Favorited != true) + throw new WebApiException("NG(Restricted?)"); + } + + this.IsFav = true; + } + + public override async Task UnfavoriteAsync() + { + var statusId = this.RetweetedId ?? this.StatusId; + + await this.twitter.Api.FavoritesDestroy(statusId) + .IgnoreResponse() + .ConfigureAwait(false); + + this.IsFav = false; + } + + public override Task RetweetAsync(SettingCommon settingCommon) + { + var statusId = this.RetweetedId ?? this.StatusId; + + var read = !settingCommon.UnreadManage || settingCommon.Read; + + return this.twitter.PostRetweet(statusId, read); + } + + public override Task DeleteAsync() + { + if (this.RetweetedByUserId == this.twitter.UserId) + { + // 自分が RT したツイート (自分が RT した自分のツイートも含む) + // => RT を取り消し + return this.twitter.Api.StatusesDestroy(this.StatusId) + .IgnoreResponse(); + } + else + { + if (this.UserId != this.twitter.UserId) + throw new InvalidOperationException(); + + if (this.RetweetedId != null) + { + // 他人に RT された自分のツイート + // => RT 元の自分のツイートを削除 + return this.twitter.Api.StatusesDestroy(this.RetweetedId.Value) + .IgnoreResponse(); + } + else + { + // 自分のツイート + // => ツイートを削除 + return this.twitter.Api.StatusesDestroy(this.StatusId) + .IgnoreResponse(); + } + } + } + } +} diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 26fa6d167..0ab93948f 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -1455,26 +1455,7 @@ await Task.Run(async () => try { - try - { - await this.tw.Api.FavoritesCreate(post.RetweetedId ?? post.StatusId) - .IgnoreResponse() - .ConfigureAwait(false); - } - catch (TwitterApiException ex) - when (ex.Errors.All(x => x.Code == TwitterErrorCode.AlreadyFavorited)) - { - // エラーコード 139 のみの場合は成功と見なす - } - - if (this.settings.Common.RestrictFavCheck) - { - var status = await this.tw.Api.StatusesShow(post.RetweetedId ?? post.StatusId) - .ConfigureAwait(false); - - if (status.Favorited != true) - throw new WebApiException("NG(Restricted?)"); - } + await post.FavoriteAsync(this.settings.Common).ConfigureAwait(false); this.favTimestamps.Add(DateTimeUtc.Now); @@ -1583,9 +1564,7 @@ await Task.Run(async () => try { - await this.tw.Api.FavoritesDestroy(post.RetweetedId ?? post.StatusId) - .IgnoreResponse() - .ConfigureAwait(false); + await post.UnfavoriteAsync().ConfigureAwait(false); } catch (WebApiException) { @@ -1791,7 +1770,7 @@ await Task.Run(async () => await this.RefreshTabAsync(); } - private async Task RetweetAsync(IReadOnlyList statusIds) + private async Task RetweetAsync(IReadOnlyList posts) { await this.workerSemaphore.WaitAsync(); @@ -1800,7 +1779,7 @@ private async Task RetweetAsync(IReadOnlyList statusIds) var progress = new Progress(x => this.StatusLabel.Text = x); this.RefreshTasktrayIcon(); - await this.RetweetAsyncInternal(progress, this.workerCts.Token, statusIds); + await this.RetweetAsyncInternal(progress, this.workerCts.Token, posts); } catch (WebApiException ex) { @@ -1813,7 +1792,7 @@ private async Task RetweetAsync(IReadOnlyList statusIds) } } - private async Task RetweetAsyncInternal(IProgress p, CancellationToken ct, IReadOnlyList statusIds) + private async Task RetweetAsyncInternal(IProgress p, CancellationToken ct, IReadOnlyList posts) { if (ct.IsCancellationRequested) return; @@ -1821,24 +1800,16 @@ private async Task RetweetAsyncInternal(IProgress p, CancellationToken c if (!CheckAccountValid()) throw new WebApiException("Auth error. Check your account"); - bool read; - if (!this.settings.Common.UnreadManage) - read = true; - else - read = this.initial && this.settings.Common.Read; - p.Report("Posting..."); - var posts = new List(); + var retweetedPosts = new List(); - await Task.Run(async () => + foreach (var post in posts) { - foreach (var statusId in statusIds) - { - var post = await this.tw.PostRetweet(statusId, read).ConfigureAwait(false); - if (post != null) posts.Add(post); - } - }); + var retweetedPost = await post.RetweetAsync(this.settings.Common).ConfigureAwait(false); + if (retweetedPost != null) + retweetedPosts.Add(retweetedPost); + } if (ct.IsCancellationRequested) return; @@ -1856,7 +1827,7 @@ await Task.Run(async () => // 自分のRTはTLの更新では取得できない場合があるので、 // 投稿時取得の有無に関わらず追加しておく - posts.ForEach(post => this.statuses.AddPost(post)); + retweetedPosts.ForEach(post => this.statuses.AddPost(post)); if (this.settings.Common.PostAndGet) { @@ -2384,40 +2355,7 @@ private async Task DoStatusDelete() try { - if (post.IsDm) - { - await this.tw.Api.DirectMessagesEventsDestroy(post.StatusId.ToString(CultureInfo.InvariantCulture)); - } - else - { - if (post.RetweetedByUserId == this.tw.UserId) - { - // 自分が RT したツイート (自分が RT した自分のツイートも含む) - // => RT を取り消し - await this.tw.Api.StatusesDestroy(post.StatusId) - .IgnoreResponse(); - } - else - { - if (post.UserId == this.tw.UserId) - { - if (post.RetweetedId != null) - { - // 他人に RT された自分のツイート - // => RT 元の自分のツイートを削除 - await this.tw.Api.StatusesDestroy(post.RetweetedId.Value) - .IgnoreResponse(); - } - else - { - // 自分のツイート - // => ツイートを削除 - await this.tw.Api.StatusesDestroy(post.StatusId) - .IgnoreResponse(); - } - } - } - } + await post.DeleteAsync(); } catch (WebApiException ex) { @@ -8118,9 +8056,7 @@ private async Task DoReTweetOfficial(bool isConfirm) } } - var statusIds = selectedPosts.Select(x => x.StatusId).ToList(); - - await this.RetweetAsync(statusIds); + await this.RetweetAsync(selectedPosts); } } diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index d5aa0419a..e3ff71241 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -632,7 +632,7 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status) => this.CreatePostsFromStatusData(status, favTweet: false); private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) - => this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet); + => this.postFactory.CreateFromStatus(this, status, this.UserId, this.followerId, favTweet); private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read) { @@ -1010,7 +1010,7 @@ private void CreateDirectMessagesEventFromJson( foreach (var eventItem in events) { - var post = this.postFactory.CreateFromDirectMessageEvent(eventItem, users, apps, this.UserId); + var post = this.postFactory.CreateFromDirectMessageEvent(this, eventItem, users, apps, this.UserId); post.IsRead = read; if (post.IsMe && !read && this.ReadOwnPost) From ce52bf09d1a1d6527d681130118cd5faf809f623 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 10 Jun 2017 04:13:39 +0900 Subject: [PATCH 06/25] =?UTF-8?q?PostClass.IsRetweet=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0,=20=E5=85=AC=E5=BC=8FRT=E3=81=A7=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=83=84=E3=82=A4=E3=83=BC=E3=83=88=E3=81=AERetweetedId?= =?UTF-8?q?=E3=82=92=E5=8F=82=E7=85=A7=E3=81=99=E3=82=8B=E3=81=A8=E4=BE=8B?= =?UTF-8?q?=E5=A4=96=E3=82=92=E8=BF=94=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RetweetedId, RetweetedBy, RetwetedByUserId は null を返さなくなる代わりに、 公式 RT でないツイートで参照すると InvalidOperationException が発生します --- OpenTween.Tests/Models/PostClassTest.cs | 15 ++-- OpenTween.Tests/Models/PostFilterRuleTest.cs | 80 +++++++++---------- OpenTween.Tests/Models/TabInformationTest.cs | 2 + .../Models/TwitterPostFactoryTest.cs | 10 +-- OpenTween.Tests/MyCommonTest.cs | 2 +- OpenTween/Models/PostClass.cs | 64 +++++++++++---- OpenTween/Models/PostFilterRule.cs | 22 +++-- OpenTween/Models/TabInformations.cs | 12 +-- OpenTween/Models/TwitterPostFactory.cs | 7 +- OpenTween/Models/TwitterStatusPost.cs | 12 +-- OpenTween/MyCommon.cs | 4 +- OpenTween/TimelineListViewCache.cs | 4 +- OpenTween/Tween.cs | 80 +++++++++++++------ OpenTween/TweetDetailsView.cs | 12 ++- OpenTween/Twitter.cs | 9 +-- 15 files changed, 197 insertions(+), 138 deletions(-) diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index eebcf63e4..423e6d596 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -57,7 +57,7 @@ protected override PostClass RetweetSource { get { - var retweetedId = this.RetweetedId!.Value; + var retweetedId = this.RetweetedId; var group = this.Group; if (group == null) throw new InvalidOperationException("TestPostClass needs group"); @@ -115,8 +115,8 @@ public void SetIsFavTest(long statusId, bool isFav) post.IsFav = isFav; Assert.Equal(isFav, post.IsFav); - if (post.RetweetedId != null) - Assert.Equal(isFav, this.postGroup[post.RetweetedId.Value].IsFav); + if (post.IsRetweet) + Assert.Equal(isFav, this.postGroup[post.RetweetedId].IsFav); } #pragma warning disable SA1008 // Opening parenthesis should be spaced correctly @@ -274,6 +274,7 @@ public void CanDeleteBy_RetweetedByMeTest() { var post = new TestPostClass { + RetweetedId = 100L, RetweetedByUserId = 111L, // 自分がリツイートした UserId = 222L, // 他人のツイート }; @@ -286,6 +287,7 @@ public void CanDeleteBy_RetweetedByOthersTest() { var post = new TestPostClass { + RetweetedId = 100L, RetweetedByUserId = 333L, // 他人がリツイートした UserId = 222L, // 他人のツイート }; @@ -298,6 +300,7 @@ public void CanDeleteBy_MyTweetHaveBeenRetweetedByOthersTest() { var post = new TestPostClass { + RetweetedId = 100L, RetweetedByUserId = 222L, // 他人がリツイートした UserId = 111L, // 自分のツイート }; @@ -386,9 +389,7 @@ public void ConvertToOriginalPost_Test() Assert.Equal("@aaa", originalPost.ScreenName); Assert.Equal(1L, originalPost.UserId); - Assert.Null(originalPost.RetweetedId); - Assert.Equal("", originalPost.RetweetedBy); - Assert.Null(originalPost.RetweetedByUserId); + Assert.False(originalPost.IsRetweet); Assert.Equal(1, originalPost.RetweetedCount); } @@ -396,7 +397,7 @@ public void ConvertToOriginalPost_Test() public void ConvertToOriginalPost_ErrorTest() { // 公式 RT でないツイート - var post = new PostClass { StatusId = 100L, RetweetedId = null }; + var post = new PostClass { StatusId = 100L }; Assert.Throws(() => post.ConvertToOriginalPost()); } diff --git a/OpenTween.Tests/Models/PostFilterRuleTest.cs b/OpenTween.Tests/Models/PostFilterRuleTest.cs index 5c6846ad7..d625afc79 100644 --- a/OpenTween.Tests/Models/PostFilterRuleTest.cs +++ b/OpenTween.Tests/Models/PostFilterRuleTest.cs @@ -171,10 +171,10 @@ public void FilterName_Test() Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // FilterName は RetweetedBy にもマッチする - post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "foo", RetweetedBy = "bar" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "bar", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // FilterName は完全一致 (UseRegex = false の場合) @@ -209,10 +209,10 @@ public void ExFilterName_Test() Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // ExFilterName は RetweetedBy にもマッチする - post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "foo", RetweetedBy = "bar" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "bar", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // ExFilterName は完全一致 (ExUseRegex = false の場合) @@ -248,10 +248,10 @@ public void FilterName_RegexTest() Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // FilterName は RetweetedBy にもマッチする - post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "foo", RetweetedBy = "bar" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "bar", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // FilterName は部分一致 (UseRegex = true の場合) @@ -287,10 +287,10 @@ public void ExFilterName_RegexTest() Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // ExFilterName は RetweetedBy にもマッチする - post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "hogehoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "foo", RetweetedBy = "bar" }; + post = new PostClass { ScreenName = "foo", RetweetedBy = "bar", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // ExFilterName は部分一致 (ExUseRegex = true の場合) @@ -645,11 +645,11 @@ public void FilterBodyAndName_Test() Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // TextFromApi と RetweetedBy に FilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // ScreenName に対しては完全一致 (UseRegex = false の場合) @@ -666,7 +666,7 @@ public void FilterBodyAndName_Test() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -675,7 +675,7 @@ public void FilterBodyAndName_Test() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); } @@ -710,11 +710,11 @@ public void ExFilterBodyAndName_Test() Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // TextFromApi と RetweetedBy に ExFilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // ScreenName に対しては完全一致 (ExUseRegex = false の場合) @@ -731,7 +731,7 @@ public void ExFilterBodyAndName_Test() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -740,7 +740,7 @@ public void ExFilterBodyAndName_Test() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); } @@ -776,11 +776,11 @@ public void FilterBodyAndName_RegexTest() Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // TextFromApi と RetweetedBy に FilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // ScreenName に対しても部分一致 (UseRegex = true の場合) @@ -797,7 +797,7 @@ public void FilterBodyAndName_RegexTest() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -806,7 +806,7 @@ public void FilterBodyAndName_RegexTest() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); } @@ -842,11 +842,11 @@ public void ExFilterBodyAndName_RegexTest() Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // TextFromApi と RetweetedBy に ExFilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "bbb", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", TextFromApi = "bbb", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // ScreenName に対しても部分一致 (ExUseRegex = true の場合) @@ -863,7 +863,7 @@ public void ExFilterBodyAndName_RegexTest() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -872,7 +872,7 @@ public void ExFilterBodyAndName_RegexTest() post = new PostClass { ScreenName = "Aaa", TextFromApi = "Bbb" }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", TextFromApi = "Bbb", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); } @@ -910,11 +910,11 @@ public void FilterBodyAndName_ByUrlTest() Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // Text と ScreenName に FilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // ScreenName に対しては完全一致 (UseRegex = false の場合) @@ -931,7 +931,7 @@ public void FilterBodyAndName_ByUrlTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -940,7 +940,7 @@ public void FilterBodyAndName_ByUrlTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); } @@ -978,11 +978,11 @@ public void ExFilterBodyAndName_ByUrlTest() Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // Text と ScreenName に ExFilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // ScreenName に対しては完全一致 (ExUseRegex = false の場合) @@ -999,7 +999,7 @@ public void ExFilterBodyAndName_ByUrlTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -1008,7 +1008,7 @@ public void ExFilterBodyAndName_ByUrlTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); } @@ -1047,11 +1047,11 @@ public void FilterBodyAndName_ByUrlRegexTest() Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // Text と ScreenName に FilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); // ScreenName に対しても部分一致 (UseRegex = true の場合) @@ -1068,7 +1068,7 @@ public void FilterBodyAndName_ByUrlRegexTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -1077,7 +1077,7 @@ public void FilterBodyAndName_ByUrlRegexTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); } @@ -1116,11 +1116,11 @@ public void ExFilterBodyAndName_ByUrlRegexTest() Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // Text と ScreenName に ExFilterBody の文字列がそれぞれ含まれている - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // RetweetedBy が null でなくても依然として ScreenName にはマッチする - post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge" }; + post = new PostClass { ScreenName = "aaa", Text = "t.co/hoge", RetweetedBy = "hoge", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); // ScreenName に対しても部分一致 (ExUseRegex = true の場合) @@ -1137,7 +1137,7 @@ public void ExFilterBodyAndName_ByUrlRegexTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.None, filter.ExecFilter(post)); // 大小文字を区別しない @@ -1146,7 +1146,7 @@ public void ExFilterBodyAndName_ByUrlRegexTest() post = new PostClass { ScreenName = "Aaa", Text = "t.co/hoge" }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); - post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa" }; + post = new PostClass { ScreenName = "hoge", Text = "t.co/hoge", RetweetedBy = "Aaa", RetweetedId = 100L }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); } diff --git a/OpenTween.Tests/Models/TabInformationTest.cs b/OpenTween.Tests/Models/TabInformationTest.cs index 4545169f6..b6aeb2e7c 100644 --- a/OpenTween.Tests/Models/TabInformationTest.cs +++ b/OpenTween.Tests/Models/TabInformationTest.cs @@ -260,6 +260,7 @@ public void IsMuted_RetweetTest() var post = new PostClass { UserId = 11111L, + RetweetedId = 100L, RetweetedByUserId = 12345L, Text = "hogehoge", }; @@ -274,6 +275,7 @@ public void IsMuted_RetweetNotMutingTest() var post = new PostClass { UserId = 11111L, + RetweetedId = 100L, RetweetedByUserId = 22222L, Text = "hogehoge", }; diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index 8c589a239..d3ea9e551 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -108,10 +108,7 @@ public void CreateFromStatus_Test() Assert.Null(post.InReplyToStatusId); Assert.Null(post.InReplyToUserId); Assert.Null(post.InReplyToUser); - - Assert.Null(post.RetweetedId); - Assert.Null(post.RetweetedBy); - Assert.Null(post.RetweetedByUserId); + Assert.False(post.IsRetweet); Assert.Equal(status.User.Id, post.UserId); Assert.Equal("tetete", post.ScreenName); @@ -267,10 +264,7 @@ public void CreateFromDirectMessageEvent_Test() Assert.Null(post.InReplyToStatusId); Assert.Null(post.InReplyToUserId); Assert.Null(post.InReplyToUser); - - Assert.Null(post.RetweetedId); - Assert.Null(post.RetweetedBy); - Assert.Null(post.RetweetedByUserId); + Assert.False(post.IsRetweet); Assert.Equal(otherUser.Id, post.UserId); Assert.Equal("tetete", post.ScreenName); diff --git a/OpenTween.Tests/MyCommonTest.cs b/OpenTween.Tests/MyCommonTest.cs index dff7e0a92..45e286251 100644 --- a/OpenTween.Tests/MyCommonTest.cs +++ b/OpenTween.Tests/MyCommonTest.cs @@ -189,7 +189,7 @@ public void GetReadableVersionTest(string fileVersion, string expected) public static readonly TheoryData GetStatusUrlTest1TestCase = new() { { - new PostClass { StatusId = 249493863826350080L, ScreenName = "Favstar_LM", RetweetedId = null, RetweetedBy = null }, + new PostClass { StatusId = 249493863826350080L, ScreenName = "Favstar_LM" }, "https://twitter.com/Favstar_LM/status/249493863826350080" }, { diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index 0a1f3a9e5..fad1087e1 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -110,17 +110,11 @@ public string Text public bool FilterHit { get; set; } - public string? RetweetedBy { get; set; } - - public long? RetweetedId { get; set; } - private bool isDeleted = false; private StatusGeo? postGeo = null; public int RetweetedCount { get; set; } - public long? RetweetedByUserId { get; set; } - public long? InReplyToUserId { get; set; } public List Media { get; set; } @@ -187,6 +181,42 @@ object ICloneable.Clone() public int FavoritedCount { get; set; } + private long? retweetedId; + private long? retweetedByUserId; + private string? retweetedBy; + + public bool IsRetweet + { + get => this.retweetedId != null; + set + { + if (value) + throw new InvalidOperationException(); + + this.retweetedId = null; + this.retweetedBy = null; + this.retweetedByUserId = null; + } + } + + public long RetweetedId + { + get => this.retweetedId ?? throw new InvalidOperationException(); + set => this.retweetedId = value; + } + + public long RetweetedByUserId + { + get => this.retweetedByUserId ?? throw new InvalidOperationException(); + set => this.retweetedByUserId = value; + } + + public string RetweetedBy + { + get => this.retweetedBy ?? throw new InvalidOperationException(); + set => this.retweetedBy = value; + } + private States states = States.None; private bool expandComplatedAll = false; @@ -215,7 +245,7 @@ public bool IsFav { get { - if (this.RetweetedId != null) + if (this.IsRetweet) { var post = this.RetweetSource; if (post != null) @@ -230,7 +260,7 @@ public bool IsFav set { this.isFav = value; - if (this.RetweetedId != null) + if (this.IsRetweet) { var post = this.RetweetSource; if (post != null) @@ -302,7 +332,7 @@ public bool IsDeleted } protected virtual PostClass? RetweetSource - => this.RetweetedId != null ? TabInformations.GetInstance().RetweetSource(this.RetweetedId.Value) : null; + => this.IsRetweet ? TabInformations.GetInstance().RetweetSource(this.RetweetedId) : null; public StatusGeo? PostGeo { @@ -355,7 +385,7 @@ public bool CanDeleteBy(long selfUserId) return true; // 自分が RT したツイート - if (this.RetweetedByUserId == selfUserId) + if (this.IsRetweet && this.RetweetedByUserId == selfUserId) return true; return false; @@ -381,15 +411,15 @@ public bool CanRetweetBy(long selfUserId) public PostClass ConvertToOriginalPost() { - if (this.RetweetedId == null) + if (!this.IsRetweet) throw new InvalidOperationException(); var originalPost = this.Clone(); - originalPost.StatusId = this.RetweetedId.Value; - originalPost.RetweetedId = null; - originalPost.RetweetedBy = ""; - originalPost.RetweetedByUserId = null; + originalPost.retweetedId = null; + originalPost.retweetedBy = ""; + originalPost.retweetedByUserId = null; + originalPost.StatusId = this.RetweetedId; originalPost.RetweetedCount = 1; return originalPost; @@ -494,8 +524,8 @@ public bool Equals(PostClass? other) (this.IsDm == other.IsDm) && (this.UserId == other.UserId) && (this.FilterHit == other.FilterHit) && - (this.RetweetedBy == other.RetweetedBy) && - (this.RetweetedId == other.RetweetedId) && + (this.retweetedBy == other.retweetedBy) && + (this.retweetedId == other.retweetedId) && (this.IsDeleted == other.IsDeleted) && (this.InReplyToUserId == other.InReplyToUserId); } diff --git a/OpenTween/Models/PostFilterRule.cs b/OpenTween/Models/PostFilterRule.cs index ad6e52bad..ec3b7e7fd 100644 --- a/OpenTween/Models/PostFilterRule.cs +++ b/OpenTween/Models/PostFilterRule.cs @@ -377,7 +377,11 @@ public void Compile() { filterExprs.Add(Expression.OrElse( this.MakeGenericFilter(postParam, "ScreenName", filterName, useRegex, caseSensitive, exactMatch: true), - this.MakeGenericFilter(postParam, "RetweetedBy", filterName, useRegex, caseSensitive, exactMatch: true))); + Expression.AndAlso( + Expression.Property(postParam, "IsRetweet"), + this.MakeGenericFilter(postParam, "RetweetedBy", filterName, useRegex, caseSensitive, exactMatch: true) + ) + )); } foreach (var body in filterBody) { @@ -405,15 +409,11 @@ public void Compile() bodyExpr, this.MakeGenericFilter(postParam, "ScreenName", body, useRegex, caseSensitive, exactMatch: true)); - // bodyExpr || x.RetweetedBy != null && + // bodyExpr || x.IsRetweet && bodyExpr = Expression.OrElse( bodyExpr, Expression.AndAlso( - Expression.NotEqual( - Expression.Property( - postParam, - typeof(PostClass).GetProperty("RetweetedBy")), - Expression.Constant(null)), + Expression.Property(postParam, "IsRetweet"), this.MakeGenericFilter(postParam, "RetweetedBy", body, useRegex, caseSensitive, exactMatch: true))); } } @@ -429,12 +429,8 @@ public void Compile() } if (filterRt) { - // x.RetweetedId != null - filterExprs.Add(Expression.NotEqual( - Expression.Property( - postParam, - typeof(PostClass).GetProperty("RetweetedId")), - Expression.Constant(null))); + // x.IsRetweet + filterExprs.Add(Expression.Property(postParam, "IsRetweet")); } if (filterExprs.Count == 0) diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index 1eac80f61..cc3bbb4bc 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -566,7 +566,7 @@ public void AddPost(PostClass item) { if (item.IsFav) { - if (item.RetweetedId == null) + if (!item.IsRetweet) { status.IsFav = true; } @@ -582,10 +582,10 @@ public void AddPost(PostClass item) } else { - if (item.IsFav && item.RetweetedId != null) item.IsFav = false; + if (item.IsFav && item.IsRetweet) item.IsFav = false; // 既に持っている公式RTは捨てる - if (item.RetweetedId != null && SettingManager.Instance.Common.HideDuplicatedRetweets) + if (item.IsRetweet && SettingManager.Instance.Common.HideDuplicatedRetweets) { var retweetCount = this.UpdateRetweetCount(item); @@ -626,7 +626,7 @@ public bool IsMuted(PostClass post, bool isHomeTimeline) if (this.MuteUserIds.Contains(post.UserId)) return true; - if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value)) + if (post.IsRetweet && this.MuteUserIds.Contains(post.RetweetedByUserId)) return true; return false; @@ -634,10 +634,10 @@ public bool IsMuted(PostClass post, bool isHomeTimeline) private int UpdateRetweetCount(PostClass retweetPost) { - if (retweetPost.RetweetedId == null) + if (!retweetPost.IsRetweet) throw new InvalidOperationException(); - var retweetedId = retweetPost.RetweetedId.Value; + var retweetedId = retweetPost.RetweetedId; return this.retweetsCount.AddOrUpdate(retweetedId, 1, (k, v) => v >= 10 ? 1 : v + 1); } diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index eeb502971..aeb8b1dc1 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -188,7 +188,7 @@ public PostClass CreateFromStatus( this.ExtractEntities(entities, post.ReplyToList, post.Media); post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink) - .Where(x => x != post.StatusId && x != post.RetweetedId) + .Where(x => x != post.StatusId && !(post.IsRetweet && x == post.RetweetedId)) .Distinct().ToArray(); post.ExpandedUrls = entities.OfType() @@ -205,14 +205,15 @@ public PostClass CreateFromStatus( post.ScreenName = string.Intern(post.ScreenName); post.Nickname = string.Intern(post.Nickname); post.ImageUrl = string.Intern(post.ImageUrl); - post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null; + if (post.IsRetweet) + post.RetweetedBy = string.Intern(post.RetweetedBy); // Source整形 var (sourceText, sourceUri) = ParseSource(sourceHtml); post.Source = string.Intern(sourceText); post.SourceUri = sourceUri; - post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == selfUserId); + post.IsReply = !post.IsRetweet && post.ReplyToList.Any(x => x.UserId == selfUserId); post.IsExcludeReply = false; if (post.IsMe) diff --git a/OpenTween/Models/TwitterStatusPost.cs b/OpenTween/Models/TwitterStatusPost.cs index d217c1425..3c6ac44fc 100644 --- a/OpenTween/Models/TwitterStatusPost.cs +++ b/OpenTween/Models/TwitterStatusPost.cs @@ -40,7 +40,7 @@ public TwitterStatusPost(Twitter twitter) public override async Task FavoriteAsync(SettingCommon settingCommon) { - var statusId = this.RetweetedId ?? this.StatusId; + var statusId = this.IsRetweet ? this.RetweetedId : this.StatusId; try { @@ -68,7 +68,7 @@ await this.twitter.Api.FavoritesCreate(statusId) public override async Task UnfavoriteAsync() { - var statusId = this.RetweetedId ?? this.StatusId; + var statusId = this.IsRetweet ? this.RetweetedId : this.StatusId; await this.twitter.Api.FavoritesDestroy(statusId) .IgnoreResponse() @@ -79,7 +79,7 @@ await this.twitter.Api.FavoritesDestroy(statusId) public override Task RetweetAsync(SettingCommon settingCommon) { - var statusId = this.RetweetedId ?? this.StatusId; + var statusId = this.IsRetweet ? this.RetweetedId : this.StatusId; var read = !settingCommon.UnreadManage || settingCommon.Read; @@ -88,7 +88,7 @@ await this.twitter.Api.FavoritesDestroy(statusId) public override Task DeleteAsync() { - if (this.RetweetedByUserId == this.twitter.UserId) + if (this.IsRetweet && this.RetweetedByUserId == this.twitter.UserId) { // 自分が RT したツイート (自分が RT した自分のツイートも含む) // => RT を取り消し @@ -100,11 +100,11 @@ public override Task DeleteAsync() if (this.UserId != this.twitter.UserId) throw new InvalidOperationException(); - if (this.RetweetedId != null) + if (this.IsRetweet) { // 他人に RT された自分のツイート // => RT 元の自分のツイートを削除 - return this.twitter.Api.StatusesDestroy(this.RetweetedId.Value) + return this.twitter.Api.StatusesDestroy(this.RetweetedId) .IgnoreResponse(); } else diff --git a/OpenTween/MyCommon.cs b/OpenTween/MyCommon.cs index ba9d2d90a..6ac6d05eb 100644 --- a/OpenTween/MyCommon.cs +++ b/OpenTween/MyCommon.cs @@ -845,10 +845,10 @@ public static string GetReadableVersion(Version version) public static string GetStatusUrl(PostClass post) { - if (post.RetweetedId == null) + if (!post.IsRetweet) return GetStatusUrl(post.ScreenName, post.StatusId); else - return GetStatusUrl(post.ScreenName, post.RetweetedId.Value); + return GetStatusUrl(post.ScreenName, post.RetweetedId); } public static string GetStatusUrl(string screenName, long statusId) diff --git a/OpenTween/TimelineListViewCache.cs b/OpenTween/TimelineListViewCache.cs index c35b95284..0c2072540 100644 --- a/OpenTween/TimelineListViewCache.cs +++ b/OpenTween/TimelineListViewCache.cs @@ -143,7 +143,7 @@ public void PurgeCache() if (post.FavoritedCount > 0) mk.Append("+" + post.FavoritedCount); ListViewItem itm; - if (post.RetweetedId == null) + if (!post.IsRetweet) { string[] sitem = { @@ -333,7 +333,7 @@ private ListItemForeColor DetermineForeColor(PostClass post, bool useUnreadStyle if (post.IsFav) return ListItemForeColor.Fav; - if (post.RetweetedId != null) + if (post.IsRetweet) return ListItemForeColor.Retweet; if (post.IsOwl && (post.IsDm || this.settings.OneWayLove)) diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 0ab93948f..c2e539954 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -2255,8 +2255,8 @@ private void ContextMenuOperate_Opening(object sender, CancelEventArgs e) this.UnreadStripMenuItem.Enabled = true; this.AuthorContextMenuItem.Visible = true; this.AuthorContextMenuItem.Text = $"@{post!.ScreenName}"; - this.RetweetedByContextMenuItem.Visible = post.RetweetedByUserId != null; - this.RetweetedByContextMenuItem.Text = $"@{post.RetweetedBy}"; + this.RetweetedByContextMenuItem.Visible = post.IsRetweet; + this.RetweetedByContextMenuItem.Text = post.IsRetweet ? $"@{post.RetweetedBy}" : ""; } var tab = this.CurrentTab; if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm) @@ -2309,7 +2309,7 @@ private void ContextMenuOperate_Opening(object sender, CancelEventArgs e) if (this.ExistCurrentPost && post != null) { this.DeleteStripMenuItem.Enabled = post.CanDeleteBy(this.tw.UserId); - if (post.RetweetedByUserId == this.tw.UserId) + if (post.IsRetweet && post.RetweetedByUserId == this.tw.UserId) this.DeleteStripMenuItem.Text = Properties.Resources.DeleteMenuText2; else this.DeleteStripMenuItem.Text = Properties.Resources.DeleteMenuText1; @@ -4833,7 +4833,7 @@ private void CopyStot() if (post.IsDeleted) continue; if (!isDm) { - if (post.RetweetedId != null) + if (post.IsRetweet) sb.AppendFormat("{0}:{1} [https://twitter.com/{0}/status/{2}]{3}", post.ScreenName, post.TextSingleLine, post.RetweetedId, Environment.NewLine); else sb.AppendFormat("{0}:{1} [https://twitter.com/{0}/status/{2}]{3}", post.ScreenName, post.TextSingleLine, post.StatusId, Environment.NewLine); @@ -5030,7 +5030,7 @@ private void GoPost(bool forward) } string name; - if (currentPost.RetweetedBy == null) + if (!currentPost.IsRetweet) { name = currentPost.ScreenName; } @@ -5041,7 +5041,7 @@ private void GoPost(bool forward) for (var idx = fIdx; idx != toIdx; idx += stp) { var post = tab[idx]; - if (post.RetweetedId == null) + if (!post.IsRetweet) { if (post.ScreenName == name) { @@ -5100,17 +5100,49 @@ private void GoRelPost(bool forward) tab.AnchorPost = currentPost; } + bool IsRelatedPost(PostClass anchorPost, PostClass targetPost) + { + if (anchorPost.UserId == targetPost.UserId) + return true; + + if (anchorPost.ReplyToList.Any(x => x.UserId == targetPost.UserId)) + return true; + + if (targetPost.ReplyToList.Any(x => x.UserId == anchorPost.UserId)) + return true; + + if (anchorPost.IsRetweet) + { + if (anchorPost.RetweetedByUserId == targetPost.UserId) + return true; + + if (targetPost.ReplyToList.Any(x => x.UserId == anchorPost.RetweetedByUserId)) + return true; + } + + if (targetPost.IsRetweet) + { + if (anchorPost.UserId == targetPost.RetweetedByUserId) + return true; + + if (anchorPost.ReplyToList.Any(x => x.UserId == targetPost.RetweetedByUserId)) + return true; + } + + if (anchorPost.IsRetweet && targetPost.IsRetweet) + { + if (anchorPost.RetweetedByUserId == targetPost.RetweetedByUserId) + return true; + } + + return false; + } + for (var idx = fIdx; idx != toIdx; idx += stp) { var post = tab[idx]; - if (post.ScreenName == anchorPost.ScreenName || - post.RetweetedBy == anchorPost.ScreenName || - post.ScreenName == anchorPost.RetweetedBy || - (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == anchorPost.RetweetedBy) || - anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) || - anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) || - post.ReplyToList.Any(x => x.UserId == anchorPost.UserId) || - post.ReplyToList.Any(x => x.UserId == anchorPost.RetweetedByUserId)) + + if (IsRelatedPost(anchorPost, post)) { var listView = this.CurrentListView; this.SelectListItem(listView, idx); @@ -6039,7 +6071,7 @@ private void MakeReplyText(bool atAll = false) if (selectedPosts.Length == 1) { var post = selectedPosts.Single(); - var inReplyToStatusId = post.RetweetedId ?? post.StatusId; + var inReplyToStatusId = post.IsRetweet ? post.RetweetedId : post.StatusId; var inReplyToScreenName = post.ScreenName; this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); } @@ -6420,7 +6452,7 @@ private void TabMenuItem_Click(object sender, EventArgs e) fltDialog.Owner = this; fltDialog.SetCurrent(tab.TabName); - if (post.RetweetedBy == null) + if (!post.IsRetweet) { fltDialog.AddNewFilter(post.ScreenName, post.TextFromApi); } @@ -6540,7 +6572,7 @@ private void IDRuleMenuItem_Click(object sender, EventArgs e) return; var screenNameArray = selectedPosts - .Select(x => x.RetweetedBy ?? x.ScreenName) + .Select(x => x.IsRetweet ? x.RetweetedBy : x.ScreenName) .ToArray(); this.AddFilterRuleByScreenName(screenNameArray); @@ -8523,7 +8555,7 @@ private void DoReTweetUnofficial() var selection = (this.StatusText.SelectionStart, this.StatusText.SelectionLength); // 投稿時に in_reply_to_status_id を付加する - var inReplyToStatusId = post.RetweetedId ?? post.StatusId; + var inReplyToStatusId = post.IsRetweet ? post.RetweetedId : post.StatusId; var inReplyToScreenName = post.ScreenName; this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); @@ -8683,7 +8715,7 @@ private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e) private async Task DoMoveToRTHome() { var post = this.CurrentPost; - if (post != null && post.RetweetedId != null) + if (post != null && post.IsRetweet) await MyCommon.OpenInBrowserAsync(this, "https://twitter.com/" + post.RetweetedBy); } @@ -8829,8 +8861,8 @@ private void MenuItemOperate_DropDownOpening(object sender, EventArgs e) this.UnreadOpMenuItem.Enabled = true; this.AuthorMenuItem.Visible = true; this.AuthorMenuItem.Text = $"@{post!.ScreenName}"; - this.RetweetedByMenuItem.Visible = post.RetweetedByUserId != null; - this.RetweetedByMenuItem.Text = $"@{post.RetweetedBy}"; + this.RetweetedByMenuItem.Visible = post.IsRetweet; + this.RetweetedByMenuItem.Text = post.IsRetweet ? $"@{post.RetweetedBy}" : ""; } if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm) @@ -9049,7 +9081,7 @@ private async void RtCountMenuItem_Click(object sender, EventArgs e) if (!this.ExistCurrentPost || post == null) return; - var statusId = post.RetweetedId ?? post.StatusId; + var statusId = post.IsRetweet ? post.RetweetedId : post.StatusId; TwitterStatus status; using (var dialog = new WaitingDialog(Properties.Resources.RtCountMenuItem_ClickText1)) @@ -9309,7 +9341,7 @@ private async Task OpenRelatedTab(PostClass post) var tabPage = this.ListTab.TabPages[tabIndex]; var listView = (DetailsListView)tabPage.Tag; var targetPost = tabRelated.TargetPost; - var index = tabRelated.IndexOf(targetPost.RetweetedId ?? targetPost.StatusId); + var index = tabRelated.IndexOf(targetPost.IsRetweet ? targetPost.RetweetedId : targetPost.StatusId); if (index != -1 && index < listView.Items.Count) { @@ -9435,7 +9467,7 @@ private async Task OpenUserAppointUrl() var xUrl = this.settings.Common.UserAppointUrl; xUrl = xUrl.Replace("{ID}", post.ScreenName); - var statusId = post.RetweetedId ?? post.StatusId; + var statusId = post.IsRetweet ? post.RetweetedId : post.StatusId; xUrl = xUrl.Replace("{STATUS}", statusId.ToString()); await MyCommon.OpenInBrowserAsync(this, xUrl); diff --git a/OpenTween/TweetDetailsView.cs b/OpenTween/TweetDetailsView.cs index f1aa2f2e3..9dc4ecba6 100644 --- a/OpenTween/TweetDetailsView.cs +++ b/OpenTween/TweetDetailsView.cs @@ -139,7 +139,7 @@ public async Task ShowPostDetails(PostClass post) nameText = ""; } nameText += post.ScreenName + "/" + post.Nickname; - if (post.RetweetedId != null) + if (post.IsRetweet) nameText += $" (RT:{post.RetweetedBy})"; this.NameLinkLabel.Text = nameText; @@ -147,7 +147,7 @@ public async Task ShowPostDetails(PostClass post) var nameForeColor = SystemColors.ControlText; if (post.IsOwl && (SettingManager.Instance.Common.OneWayLove || post.IsDm)) nameForeColor = this.Theme.ColorOWL; - if (post.RetweetedId != null) + if (post.IsRetweet) nameForeColor = this.Theme.ColorRetweet; if (post.IsFav) nameForeColor = this.Theme.ColorFav; @@ -193,8 +193,12 @@ public async Task ShowPostDetails(PostClass post) sb.AppendFormat("Source : {0}
", post.Source); sb.AppendFormat("UserId : {0}
", post.UserId); sb.AppendFormat("FilterHit : {0}
", post.FilterHit); - sb.AppendFormat("RetweetedBy : {0}
", post.RetweetedBy); - sb.AppendFormat("RetweetedId : {0}
", post.RetweetedId); + + if (post.IsRetweet) + { + sb.AppendFormat("RetweetedBy : {0}
", post.RetweetedBy); + sb.AppendFormat("RetweetedId : {0}
", post.RetweetedId); + } sb.AppendFormat("Media.Count : {0}
", post.Media.Count); if (post.Media.Count > 0) diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index e3ff71241..e3959d570 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -357,7 +357,7 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) if (post == null) throw new WebApiException("Err:Target isn't found."); - var target = post.RetweetedId ?? id; // 再RTの場合は元発言をRT + var target = post.IsRetweet ? post.RetweetedId : id; // 再RTの場合は元発言をRT var response = await this.Api.StatusesRetweet(target) .ConfigureAwait(false); @@ -773,12 +773,11 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) { var targetPost = tab.TargetPost; - if (targetPost.RetweetedId != null) + if (targetPost.IsRetweet) { var originalPost = targetPost.Clone(); - originalPost.StatusId = targetPost.RetweetedId.Value; - originalPost.RetweetedId = null; - originalPost.RetweetedBy = null; + originalPost.StatusId = targetPost.RetweetedId; + originalPost.IsRetweet = false; targetPost = originalPost; } From 73b3839b23efbe4f6d67e6812c22c2bed41592b6 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 10 Jun 2017 12:11:17 +0900 Subject: [PATCH 07/25] =?UTF-8?q?PostClass.HasInReplyTo=20=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0,=20=E3=83=AA=E3=83=97=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=81=A7=E3=81=AA=E3=81=84=E3=83=84=E3=82=A4=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=AEInReplyToStatusId=E3=82=92=E5=8F=82=E7=85=A7=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=A8=E4=BE=8B=E5=A4=96=E3=82=92=E8=BF=94=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InReplyToStatusId, InReplyToUserId, InReplyToUser は null を返さなくなる代わりに、 リプライでないツイートで参照すると InvalidOperationException が発生します --- OpenTween.Tests/Models/PostClassTest.cs | 7 +-- .../Models/TwitterPostFactoryTest.cs | 8 +-- OpenTween.Tests/TwitterTest.cs | 2 +- OpenTween/Models/PostClass.cs | 63 +++++++++++-------- OpenTween/Models/TwitterPostFactory.cs | 19 ++++-- OpenTween/TimelineListViewCache.cs | 2 +- OpenTween/Tween.cs | 42 +++++++------ OpenTween/TweetDetailsView.cs | 19 +++--- OpenTween/Twitter.cs | 16 ++--- 9 files changed, 99 insertions(+), 79 deletions(-) diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index 423e6d596..05003ee87 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -144,9 +144,10 @@ public void StateIndexTest(bool protect, bool mark, bool reply, bool geo, int ex { IsProtect = protect, IsMark = mark, - InReplyToStatusId = reply ? (long?)100L : null, PostGeo = geo ? new PostClass.StatusGeo(-126.716667, -47.15) : (PostClass.StatusGeo?)null, }; + if (reply) + post.InReplyToStatusId = 100L; Assert.Equal(expected, post.StateIndex); } @@ -213,9 +214,7 @@ public void DeleteTest() post.IsDeleted = true; - Assert.Null(post.InReplyToStatusId); - Assert.Equal("", post.InReplyToUser); - Assert.Null(post.InReplyToUserId); + Assert.False(post.HasInReplyTo); Assert.False(post.IsReply); Assert.Empty(post.ReplyToList); Assert.Equal(-1, post.StateIndex); diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index d3ea9e551..6394b704d 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -105,9 +105,7 @@ public void CreateFromStatus_Test() Assert.False(post.IsMark); Assert.False(post.IsReply); - Assert.Null(post.InReplyToStatusId); - Assert.Null(post.InReplyToUserId); - Assert.Null(post.InReplyToUser); + Assert.False(post.HasInReplyTo); Assert.False(post.IsRetweet); Assert.Equal(status.User.Id, post.UserId); @@ -261,9 +259,7 @@ public void CreateFromDirectMessageEvent_Test() Assert.False(post.IsMark); Assert.False(post.IsReply); - Assert.Null(post.InReplyToStatusId); - Assert.Null(post.InReplyToUserId); - Assert.Null(post.InReplyToUser); + Assert.False(post.HasInReplyTo); Assert.False(post.IsRetweet); Assert.Equal(otherUser.Id, post.UserId); diff --git a/OpenTween.Tests/TwitterTest.cs b/OpenTween.Tests/TwitterTest.cs index 2c03f9698..e5bb7e59a 100644 --- a/OpenTween.Tests/TwitterTest.cs +++ b/OpenTween.Tests/TwitterTest.cs @@ -88,7 +88,7 @@ public void FindTopOfReplyChainTest() { var posts = new Dictionary { - [950L] = new PostClass { StatusId = 950L, InReplyToStatusId = null }, // このツイートが末端 + [950L] = new PostClass { StatusId = 950L }, // このツイートが末端 [987L] = new PostClass { StatusId = 987L, InReplyToStatusId = 950L }, [999L] = new PostClass { StatusId = 999L, InReplyToStatusId = 987L }, [1000L] = new PostClass { StatusId = 1000L, InReplyToStatusId = 999L }, diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index fad1087e1..bf8ffb291 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -92,10 +92,6 @@ public string Text private bool isMark; - public string? InReplyToUser { get; set; } - - private long? inReplyToStatusId; - public string Source { get; set; } = ""; public Uri? SourceUri { get; set; } @@ -115,8 +111,6 @@ public string Text public int RetweetedCount { get; set; } - public long? InReplyToUserId { get; set; } - public List Media { get; set; } public long[] QuoteStatusIds { get; set; } @@ -181,10 +175,39 @@ object ICloneable.Clone() public int FavoritedCount { get; set; } + private long? inReplyToStatusId; + private long? inReplyToUserId; + private string? inReplyToUser; + private long? retweetedId; private long? retweetedByUserId; private string? retweetedBy; + public bool HasInReplyTo + => this.inReplyToStatusId != null; + + public long InReplyToStatusId + { + get => this.inReplyToStatusId ?? throw new InvalidOperationException(); + set + { + this.states |= States.Reply; + this.inReplyToStatusId = value; + } + } + + public long InReplyToUserId + { + get => this.inReplyToUserId ?? throw new InvalidOperationException(); + set => this.inReplyToUserId = value; + } + + public string InReplyToUser + { + get => this.inReplyToUser ?? throw new InvalidOperationException(); + set => this.inReplyToUser = value; + } + public bool IsRetweet { get => this.retweetedId != null; @@ -299,20 +322,6 @@ public bool IsMark } } - public long? InReplyToStatusId - { - get => this.inReplyToStatusId; - set - { - if (value != null) - this.states |= States.Reply; - else - this.states &= ~States.Reply; - - this.inReplyToStatusId = value; - } - } - public bool IsDeleted { get => this.isDeleted; @@ -320,9 +329,9 @@ public bool IsDeleted { if (value) { - this.InReplyToStatusId = null; - this.InReplyToUser = ""; - this.InReplyToUserId = null; + this.inReplyToStatusId = null; + this.inReplyToUser = ""; + this.inReplyToUserId = null; this.IsReply = false; this.ReplyToList = new List<(long, string)>(); this.states = States.None; @@ -515,8 +524,9 @@ public bool Equals(PostClass? other) (this.IsProtect == other.IsProtect) && (this.IsOwl == other.IsOwl) && (this.IsMark == other.IsMark) && - (this.InReplyToUser == other.InReplyToUser) && - (this.InReplyToStatusId == other.InReplyToStatusId) && + (this.inReplyToUser == other.inReplyToUser) && + (this.inReplyToUserId == other.inReplyToUserId) && + (this.inReplyToStatusId == other.inReplyToStatusId) && (this.Source == other.Source) && (this.SourceUri == other.SourceUri) && this.ReplyToList.SequenceEqual(other.ReplyToList) && @@ -526,8 +536,7 @@ public bool Equals(PostClass? other) (this.FilterHit == other.FilterHit) && (this.retweetedBy == other.retweetedBy) && (this.retweetedId == other.retweetedId) && - (this.IsDeleted == other.IsDeleted) && - (this.InReplyToUserId == other.InReplyToUserId); + (this.IsDeleted == other.IsDeleted); } public override int GetHashCode() diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index aeb8b1dc1..57fb8295e 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -77,9 +77,12 @@ public PostClass CreateFromStatus( entities = retweeted.MergedEntities; sourceHtml = retweeted.Source; // Reply先 - post.InReplyToStatusId = retweeted.InReplyToStatusId; - post.InReplyToUser = retweeted.InReplyToScreenName; - post.InReplyToUserId = status.InReplyToUserId; + if (retweeted.InReplyToStatusId != null) + { + post.InReplyToStatusId = retweeted.InReplyToStatusId.Value; + post.InReplyToUser = retweeted.InReplyToScreenName!; + post.InReplyToUserId = retweeted.InReplyToUserId!.Value; + } if (favTweet) { @@ -132,9 +135,13 @@ public PostClass CreateFromStatus( post.TextFromApi = status.FullText; entities = status.MergedEntities; sourceHtml = status.Source; - post.InReplyToStatusId = status.InReplyToStatusId; - post.InReplyToUser = status.InReplyToScreenName; - post.InReplyToUserId = status.InReplyToUserId; + + if (status.InReplyToStatusId != null) + { + post.InReplyToStatusId = status.InReplyToStatusId.Value; + post.InReplyToUser = status.InReplyToScreenName!; + post.InReplyToUserId = status.InReplyToUserId!.Value; + } if (favTweet) { diff --git a/OpenTween/TimelineListViewCache.cs b/OpenTween/TimelineListViewCache.cs index 0c2072540..43029b26b 100644 --- a/OpenTween/TimelineListViewCache.cs +++ b/OpenTween/TimelineListViewCache.cs @@ -301,7 +301,7 @@ private ListItemBackColor DetermineBackColor(PostClass? basePost, PostClass post return ListItemBackColor.None; // @先 - if (post.StatusId == basePost.InReplyToStatusId) + if (basePost.HasInReplyTo && post.StatusId == basePost.InReplyToStatusId) return ListItemBackColor.AtTo; // 自分=発言者 diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index c2e539954..3a552a750 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -2297,7 +2297,7 @@ private void ContextMenuOperate_Opening(object sender, CancelEventArgs e) } } - if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null) + if (!this.ExistCurrentPost || post == null || !post.HasInReplyTo) { this.RepliedStatusOpenMenuItem.Enabled = false; } @@ -5271,15 +5271,18 @@ private async Task GoInReplyToPostTree() if (currentPost == null) return; - if (curTabClass.TabType == MyCommon.TabUsageType.PublicSearch && currentPost.InReplyToStatusId == null && currentPost.TextFromApi.Contains("@")) + if (curTabClass.TabType == MyCommon.TabUsageType.PublicSearch && !currentPost.HasInReplyTo && currentPost.TextFromApi.Contains("@")) { try { var post = await this.tw.GetStatusApi(false, currentPost.StatusId); - currentPost.InReplyToStatusId = post.InReplyToStatusId; - currentPost.InReplyToUser = post.InReplyToUser; - currentPost.IsReply = post.IsReply; + if (post.HasInReplyTo) + { + currentPost.InReplyToStatusId = post.InReplyToStatusId; + currentPost.InReplyToUser = post.InReplyToUser; + currentPost.IsReply = post.IsReply; + } this.listCache?.PurgeCache(); var index = curTabClass.SelectedIndex; @@ -5291,17 +5294,17 @@ private async Task GoInReplyToPostTree() } } - if (!(this.ExistCurrentPost && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null)) return; + if (!(this.ExistCurrentPost && currentPost.HasInReplyTo)) return; if (this.replyChains == null || (this.replyChains.Count > 0 && this.replyChains.Peek().InReplyToId != currentPost.StatusId)) { this.replyChains = new Stack(); } - this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId.Value, curTabClass)); + this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId, curTabClass)); int inReplyToIndex; string inReplyToTabName; - var inReplyToId = currentPost.InReplyToStatusId.Value; + var inReplyToId = currentPost.InReplyToStatusId; var inReplyToUser = currentPost.InReplyToUser; var inReplyToPosts = from tab in this.statuses.Tabs @@ -5319,7 +5322,7 @@ from post in tab.Posts.Values { await Task.Run(async () => { - var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.Value) + var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId) .ConfigureAwait(false); post.IsRead = true; @@ -5369,11 +5372,12 @@ private void GoBackInReplyToPostTree(bool parallel = false, bool isForward = tru if (parallel) { - if (currentPost.InReplyToStatusId != null) + if (currentPost.HasInReplyTo) { var posts = from t in this.statuses.Tabs from p in t.Posts - where p.Value.StatusId != currentPost.StatusId && p.Value.InReplyToStatusId == currentPost.InReplyToStatusId + where p.Value.StatusId != currentPost.StatusId + where p.Value.HasInReplyTo && p.Value.InReplyToStatusId == currentPost.InReplyToStatusId let indexOf = t.IndexOf(p.Value.StatusId) where indexOf > -1 orderby isForward ? indexOf : indexOf * -1 @@ -5412,7 +5416,7 @@ where indexOf > -1 { var posts = from t in this.statuses.Tabs from p in t.Posts - where p.Value.InReplyToStatusId == currentPost.StatusId + where p.Value.HasInReplyTo && p.Value.InReplyToStatusId == currentPost.StatusId let indexOf = t.IndexOf(p.Value.StatusId) where indexOf > -1 orderby indexOf @@ -7276,14 +7280,14 @@ private void SplitContainer1_SplitterMoved(object sender, SplitterEventArgs e) private async Task DoRepliedStatusOpen() { var currentPost = this.CurrentPost; - if (this.ExistCurrentPost && currentPost != null && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null) + if (this.ExistCurrentPost && currentPost != null && currentPost.HasInReplyTo) { if (MyCommon.IsKeyDown(Keys.Shift)) { - await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value)); + await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId)); return; } - if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId.Value, out var repPost)) + if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId, out var repPost)) { MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname} ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi); } @@ -7291,12 +7295,12 @@ private async Task DoRepliedStatusOpen() { foreach (var tb in this.statuses.GetTabsByType(MyCommon.TabUsageType.Lists | MyCommon.TabUsageType.PublicSearch)) { - if (tb == null || !tb.Contains(currentPost.InReplyToStatusId.Value)) break; - repPost = tb.Posts[currentPost.InReplyToStatusId.Value]; + if (tb == null || !tb.Contains(currentPost.InReplyToStatusId)) break; + repPost = tb.Posts[currentPost.InReplyToStatusId]; MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname} ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi); return; } - await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value)); + await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId)); } } } @@ -8910,7 +8914,7 @@ private void MenuItemOperate_DropDownOpening(object sender, EventArgs e) { this.RefreshPrevOpMenuItem.Enabled = false; } - if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null) + if (!this.ExistCurrentPost || post == null || !post.HasInReplyTo) { this.OpenRepSourceOpMenuItem.Enabled = false; } diff --git a/OpenTween/TweetDetailsView.cs b/OpenTween/TweetDetailsView.cs index 9dc4ecba6..420bfa85c 100644 --- a/OpenTween/TweetDetailsView.cs +++ b/OpenTween/TweetDetailsView.cs @@ -169,8 +169,13 @@ public async Task ShowPostDetails(PostClass post) sb.AppendFormat("(PlainText) : {0}
", post.TextFromApi); sb.AppendFormat("StatusId : {0}
", post.StatusId); sb.AppendFormat("ImageUrl : {0}
", post.ImageUrl); - sb.AppendFormat("InReplyToStatusId : {0}
", post.InReplyToStatusId); - sb.AppendFormat("InReplyToUser : {0}
", post.InReplyToUser); + + if (post.HasInReplyTo) + { + sb.AppendFormat("InReplyToStatusId : {0}
", post.InReplyToStatusId); + sb.AppendFormat("InReplyToUser : {0}
", post.InReplyToUser); + } + sb.AppendFormat("IsDM : {0}
", post.IsDm); sb.AppendFormat("IsFav : {0}
", post.IsFav); sb.AppendFormat("IsMark : {0}
", post.IsMark); @@ -322,15 +327,15 @@ private void ClearUserPicture() private async Task AppendQuoteTweetAsync(PostClass post) { var quoteStatusIds = post.QuoteStatusIds; - if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null) + if (quoteStatusIds.Length == 0 && !post.HasInReplyTo) return; // 「読み込み中」テキストを表示 var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false)); var loadingReplyHtml = string.Empty; - if (post.InReplyToStatusId != null) - loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId.Value, Properties.Resources.LoadingText, isReply: true); + if (post.HasInReplyTo) + loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId, Properties.Resources.LoadingText, isReply: true); var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml; @@ -340,8 +345,8 @@ private async Task AppendQuoteTweetAsync(PostClass post) // 引用ツイートを読み込み var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList(); - if (post.InReplyToStatusId != null) - loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId.Value, isReply: true)); + if (post.HasInReplyTo) + loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId, isReply: true)); var quoteHtmls = await Task.WhenAll(loadTweetTasks); diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index e3959d570..b936f2636 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -759,11 +759,11 @@ internal static PostClass FindTopOfReplyChain(IDictionary posts throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId)); var nextPost = posts[startStatusId]; - while (nextPost.InReplyToStatusId != null) + while (nextPost.HasInReplyTo) { - if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value)) + if (!posts.ContainsKey(nextPost.InReplyToStatusId)) break; - nextPost = posts[nextPost.InReplyToStatusId.Value]; + nextPost = posts[nextPost.InReplyToStatusId]; } return nextPost; @@ -782,11 +782,11 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) } var relPosts = new Dictionary(); - if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null) + if (targetPost.TextFromApi.Contains("@") && !targetPost.HasInReplyTo) { // 検索結果対応 var p = TabInformations.GetInstance()[targetPost.StatusId]; - if (p != null && p.InReplyToStatusId != null) + if (p != null && p.HasInReplyTo) { targetPost = p; } @@ -804,9 +804,9 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) // in_reply_to_status_id を使用してリプライチェインを辿る var nextPost = FindTopOfReplyChain(relPosts, targetPost.StatusId); var loopCount = 1; - while (nextPost.InReplyToStatusId != null && loopCount++ <= 20) + while (nextPost.HasInReplyTo && loopCount++ <= 20) { - var inReplyToId = nextPost.InReplyToStatusId.Value; + var inReplyToId = nextPost.InReplyToStatusId; var inReplyToPost = TabInformations.GetInstance()[inReplyToId]; if (inReplyToPost == null) @@ -871,7 +871,7 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) continue; // リプライチェーンが繋がらないツイートは除外 - if (post.InReplyToStatusId == null || !relPosts.ContainsKey(post.InReplyToStatusId.Value)) + if (!post.HasInReplyTo || !relPosts.ContainsKey(post.InReplyToStatusId)) continue; relPosts.Add(post.StatusId, post); From 3c472fad0361f789e487fb1e368e03d23148845f Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 29 Apr 2017 17:28:29 +0900 Subject: [PATCH 08/25] =?UTF-8?q?Mastodon=20API=E3=81=AB=E5=AF=BE=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E9=80=81=E4=BF=A1=E3=81=99=E3=82=8BMastodonApiConnection?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Connection/IMastodonApiConnection.cs | 42 ++++ OpenTween/Connection/MastodonApiConnection.cs | 200 ++++++++++++++++++ OpenTween/Connection/Networking.cs | 9 + 3 files changed, 251 insertions(+) create mode 100644 OpenTween/Connection/IMastodonApiConnection.cs create mode 100644 OpenTween/Connection/MastodonApiConnection.cs diff --git a/OpenTween/Connection/IMastodonApiConnection.cs b/OpenTween/Connection/IMastodonApiConnection.cs new file mode 100644 index 000000000..458c6c777 --- /dev/null +++ b/OpenTween/Connection/IMastodonApiConnection.cs @@ -0,0 +1,42 @@ +// 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 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace OpenTween.Connection +{ + public interface IMastodonApiConnection : IDisposable + { + Task GetAsync(Uri uri, IEnumerable>? param); + + Task> PostLazyAsync(Uri uri, IEnumerable>? param); + + Task> PostLazyAsync(HttpMethod method, Uri uri, IEnumerable>? param); + + Task GetStreamAsync(Uri uri, IEnumerable>? param); + } +} diff --git a/OpenTween/Connection/MastodonApiConnection.cs b/OpenTween/Connection/MastodonApiConnection.cs new file mode 100644 index 000000000..55dcba41d --- /dev/null +++ b/OpenTween/Connection/MastodonApiConnection.cs @@ -0,0 +1,200 @@ +// 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 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Cache; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace OpenTween.Connection +{ + public sealed class MastodonApiConnection : IMastodonApiConnection + { + public Uri InstanceUri { get; } + + public Uri WebsocketUri { get; } + + public string? AccessToken { get; } + + internal HttpClient Http = null!; + + public MastodonApiConnection(Uri instanceUri) + : this(instanceUri, accessToken: null) + { + } + + public MastodonApiConnection(Uri instanceUri, string? accessToken) + { + this.InstanceUri = instanceUri; + this.AccessToken = accessToken; + + var websocketUri = new UriBuilder(this.InstanceUri); + websocketUri.Scheme = websocketUri.Scheme == "https" ? "wss" : "ws"; + this.WebsocketUri = websocketUri.Uri; + + this.InitializeHttpClient(); + Networking.WebProxyChanged += this.Networking_WebProxyChanged; + } + + public async Task GetAsync(Uri uri, IEnumerable>? param) + { + var requestUri = new Uri(this.InstanceUri, uri); + + if (param != null) + requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param)); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + if (!MyCommon.IsNullOrEmpty(this.AccessToken)) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.AccessToken); + + using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + using var content = response.Content; + var responseText = await content.ReadAsStringAsync() + .ConfigureAwait(false); + + try + { + return MyCommon.CreateDataFromJson(responseText); + } + catch (SerializationException ex) + { + throw new WebApiException("Invalid Response", responseText, ex); + } + } + catch (HttpRequestException ex) + { + throw new WebApiException(ex.InnerException?.Message ?? ex.Message, ex); + } + catch (OperationCanceledException ex) + { + throw new WebApiException("Timeout", ex); + } + } + + public async Task GetStreamAsync(Uri uri, IEnumerable>? param) + { + var requestUri = new Uri(this.InstanceUri, uri); + + if (param != null) + requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param)); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + if (!MyCommon.IsNullOrEmpty(this.AccessToken)) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.AccessToken); + + using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + catch (HttpRequestException ex) + { + throw new WebApiException(ex.InnerException?.Message ?? ex.Message, ex); + } + catch (OperationCanceledException ex) + { + throw new WebApiException("Timeout", ex); + } + } + + public Task> PostLazyAsync(Uri uri, IEnumerable>? param) + => this.PostLazyAsync(HttpMethod.Post, uri, param); + + public async Task> PostLazyAsync(HttpMethod method, Uri uri, IEnumerable>? param) + { + var requestUri = new Uri(this.InstanceUri, uri); + + if (param == null) + param = Enumerable.Empty>(); + + try + { + using var request = new HttpRequestMessage(method, requestUri); + using var postContent = new FormUrlEncodedContent(param); + + if (!string.IsNullOrEmpty(this.AccessToken)) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.AccessToken); + + request.Content = postContent; + + HttpResponseMessage? response = null; + try + { + response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + var result = new LazyJson(response); + response = null; + + return result; + } + finally + { + response?.Dispose(); + } + } + catch (HttpRequestException ex) + { + throw new WebApiException(ex.InnerException?.Message ?? ex.Message, ex); + } + catch (OperationCanceledException ex) + { + throw new WebApiException("Timeout", ex); + } + } + + public void Dispose() + { + Networking.WebProxyChanged -= this.Networking_WebProxyChanged; + this.Http.Dispose(); + } + + private void InitializeHttpClient() + { + var innerHandler = Networking.CreateHttpClientHandler(); + innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); + + this.Http = Networking.CreateHttpClient(innerHandler); + } + + private void Networking_WebProxyChanged(object sender, EventArgs e) + => this.InitializeHttpClient(); + } +} diff --git a/OpenTween/Connection/Networking.cs b/OpenTween/Connection/Networking.cs index 4dca4fa1c..3871c614e 100644 --- a/OpenTween/Connection/Networking.cs +++ b/OpenTween/Connection/Networking.cs @@ -28,6 +28,7 @@ using System.Net; using System.Net.Cache; using System.Net.Http; +using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -192,6 +193,14 @@ public static HttpClient CreateHttpClient(HttpMessageHandler handler) return client; } + public static void SetWebSocketOptions(ClientWebSocketOptions options) + { + if (Networking.Proxy != null) + options.Proxy = Networking.Proxy; + + options.SetRequestHeader("User-Agent", Networking.GetUserAgentString()); + } + public static string GetUserAgentString(bool fakeMSIE = false) { if (fakeMSIE) From 0bc8fe1fe48c218497d11ee14e10309f9ad7136b Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Thu, 4 May 2017 18:20:39 +0900 Subject: [PATCH 09/25] =?UTF-8?q?Mastodon=20API=E3=81=AEJSON=E3=81=AB?= =?UTF-8?q?=E5=90=AB=E3=81=BE=E3=82=8C=E3=82=8B=E3=82=A8=E3=83=B3=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=86=E3=82=A3=E3=81=AE=E5=AE=9A=E7=BE=A9=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Api/DataModel/MastodonAccount.cs | 76 ++++++++++++++++ .../Api/DataModel/MastodonApplication.cs | 37 ++++++++ OpenTween/Api/DataModel/MastodonAttachment.cs | 49 ++++++++++ OpenTween/Api/DataModel/MastodonError.cs | 34 +++++++ OpenTween/Api/DataModel/MastodonInstance.cs | 46 ++++++++++ OpenTween/Api/DataModel/MastodonMention.cs | 43 +++++++++ OpenTween/Api/DataModel/MastodonStatus.cs | 91 +++++++++++++++++++ OpenTween/Api/DataModel/MastodonTag.cs | 37 ++++++++ 8 files changed, 413 insertions(+) create mode 100644 OpenTween/Api/DataModel/MastodonAccount.cs create mode 100644 OpenTween/Api/DataModel/MastodonApplication.cs create mode 100644 OpenTween/Api/DataModel/MastodonAttachment.cs create mode 100644 OpenTween/Api/DataModel/MastodonError.cs create mode 100644 OpenTween/Api/DataModel/MastodonInstance.cs create mode 100644 OpenTween/Api/DataModel/MastodonMention.cs create mode 100644 OpenTween/Api/DataModel/MastodonStatus.cs create mode 100644 OpenTween/Api/DataModel/MastodonTag.cs diff --git a/OpenTween/Api/DataModel/MastodonAccount.cs b/OpenTween/Api/DataModel/MastodonAccount.cs new file mode 100644 index 000000000..6053430f8 --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonAccount.cs @@ -0,0 +1,76 @@ +// 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 MastodonAccount + { + [DataMember(Name = "id")] + public long Id { get; set; } + + [DataMember(Name = "username")] + public string Username { get; set; } + + [DataMember(Name = "acct")] + public string Acct { get; set; } + + [DataMember(Name = "display_name")] + public string DisplayName { get; set; } + + [DataMember(Name = "locked")] + public bool Locked { get; set; } + + [DataMember(Name = "created_at")] + public string CreatedAt { get; set; } + + [DataMember(Name = "followers_count")] + public int FollowersCount { get; set; } + + [DataMember(Name = "following_count")] + public int FollowingCount { get; set; } + + [DataMember(Name = "statuses_count")] + public int StatusesCount { get; set; } + + [DataMember(Name = "note")] + public string Note { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + + [DataMember(Name = "avatar")] + public string Avatar { get; set; } + + [DataMember(Name = "avatar_static")] + public string AvatarStatic { get; set; } + + [DataMember(Name = "header")] + public string Header { get; set; } + + [DataMember(Name = "header_static")] + public string HeaderStatic { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonApplication.cs b/OpenTween/Api/DataModel/MastodonApplication.cs new file mode 100644 index 000000000..11a15af4e --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonApplication.cs @@ -0,0 +1,37 @@ +// 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 MastodonApplication + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "website")] + public string? Website { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonAttachment.cs b/OpenTween/Api/DataModel/MastodonAttachment.cs new file mode 100644 index 000000000..52d0b9d4e --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonAttachment.cs @@ -0,0 +1,49 @@ +// 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 MastodonAttachment + { + [DataMember(Name = "id")] + public long Id { get; set; } + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + + [DataMember(Name = "remote_url")] + public string RemoteUrl { get; set; } + + [DataMember(Name = "preview_url")] + public string PreviewUrl { get; set; } + + [DataMember(Name = "text_url")] + public string TextUrl { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonError.cs b/OpenTween/Api/DataModel/MastodonError.cs new file mode 100644 index 000000000..1b5964830 --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonError.cs @@ -0,0 +1,34 @@ +// 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 MastodonError + { + [DataMember(Name = "error")] + public string Error { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonInstance.cs b/OpenTween/Api/DataModel/MastodonInstance.cs new file mode 100644 index 000000000..c93a457b7 --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonInstance.cs @@ -0,0 +1,46 @@ +// 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 MastodonInstance + { + [DataMember(Name = "uri")] + public string Uri { get; set; } + + [DataMember(Name = "title")] + public string Title { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "email")] + public string Email { get; set; } + + [DataMember(Name = "version")] + public string Version { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonMention.cs b/OpenTween/Api/DataModel/MastodonMention.cs new file mode 100644 index 000000000..b0fe382ce --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonMention.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 MastodonMention + { + [DataMember(Name = "url")] + public string Url { get; set; } + + [DataMember(Name = "username")] + public string Username { get; set; } + + [DataMember(Name = "acct")] + public string Acct { get; set; } + + [DataMember(Name = "id")] + public long Id { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonStatus.cs b/OpenTween/Api/DataModel/MastodonStatus.cs new file mode 100644 index 000000000..21dab8fc8 --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonStatus.cs @@ -0,0 +1,91 @@ +// 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 MastodonStatus + { + [DataMember(Name = "id")] + public long Id { get; set; } + + [DataMember(Name = "uri")] + public string Uri { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + + [DataMember(Name = "account")] + public MastodonAccount Account { get; set; } + + [DataMember(Name = "in_reply_to_id")] + public long? InReplyToId { get; set; } + + [DataMember(Name = "in_reply_to_account_id")] + public long? InReplyToAccountId { get; set; } + + [DataMember(Name = "reblog")] + public MastodonStatus Reblog { get; set; } + + [DataMember(Name = "content")] + public string Content { get; set; } + + [DataMember(Name = "created_at")] + public string CreatedAt { get; set; } + + [DataMember(Name = "reblogs_count")] + public int ReblogsCount { get; set; } + + [DataMember(Name = "favourites_count")] + public int FavouritesCount { get; set; } + + [DataMember(Name = "reblogged")] + public bool? Reblogged { get; set; } + + [DataMember(Name = "favourited")] + public bool? Favourited { get; set; } + + [DataMember(Name = "sensitive")] + public bool? Sensitive { get; set; } + + [DataMember(Name = "spoiler_text")] + public string SpoilerText { get; set; } + + [DataMember(Name = "visibility")] + public string Visibility { get; set; } + + [DataMember(Name = "media_attachments")] + public MastodonAttachment[] MediaAttachments { get; set; } + + [DataMember(Name = "mentions")] + public MastodonMention[] Mentions { get; set; } + + [DataMember(Name = "tags")] + public MastodonTag[] Tags { get; set; } + + [DataMember(Name = "application")] + public MastodonApplication Application { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/MastodonTag.cs b/OpenTween/Api/DataModel/MastodonTag.cs new file mode 100644 index 000000000..76d7ef68a --- /dev/null +++ b/OpenTween/Api/DataModel/MastodonTag.cs @@ -0,0 +1,37 @@ +// 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 MastodonTag + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + } +} From d25b4331a3cf7307777a2b2633cee905f46cf7c2 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 20 May 2017 03:29:23 +0900 Subject: [PATCH 10/25] =?UTF-8?q?Mastodon=20API=E3=81=A7=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=83=AC=E3=82=B9=E3=83=9D=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E8=A9=B3=E7=B4=B0=E3=82=92=E4=BE=8B=E5=A4=96=E3=83=A1?= =?UTF-8?q?=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=AB=E5=90=AB=E3=82=81?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Connection/MastodonApiConnection.cs | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/OpenTween/Connection/MastodonApiConnection.cs b/OpenTween/Connection/MastodonApiConnection.cs index 55dcba41d..3afba40a3 100644 --- a/OpenTween/Connection/MastodonApiConnection.cs +++ b/OpenTween/Connection/MastodonApiConnection.cs @@ -25,11 +25,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Cache; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.Serialization; using System.Threading.Tasks; +using OpenTween.Api.DataModel; namespace OpenTween.Connection { @@ -77,7 +79,8 @@ public async Task GetAsync(Uri uri, IEnumerable GetStreamAsync(Uri uri, IEnumerable> PostLazyAsync(HttpMethod method, Uri uri, IEnu response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + await this.CheckStatusCode(response) + .ConfigureAwait(false); var result = new LazyJson(response); response = null; @@ -186,6 +191,37 @@ public void Dispose() this.Http.Dispose(); } + private async Task CheckStatusCode(HttpResponseMessage response) + { + var statusCode = response.StatusCode; + if (statusCode == HttpStatusCode.OK) + return; + + string responseText; + using (var content = response.Content) + { + responseText = await content.ReadAsStringAsync() + .ConfigureAwait(false); + } + + if (!string.IsNullOrWhiteSpace(responseText)) + { + try + { + var error = MyCommon.CreateDataFromJson(responseText); + var errorText = error?.Error; + + if (!MyCommon.IsNullOrEmpty(errorText)) + throw new WebApiException(errorText, responseText); + } + catch (SerializationException) + { + } + } + + throw new WebApiException(statusCode.ToString(), responseText); + } + private void InitializeHttpClient() { var innerHandler = Networking.CreateHttpClientHandler(); From 3934eb3f81f82a35f5804bbd348c5bcaa1837efd Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 6 May 2017 08:28:31 +0900 Subject: [PATCH 11/25] =?UTF-8?q?SettingCommon.xml=E3=81=ABMastodon?= =?UTF-8?q?=E3=81=AE=E8=AA=8D=E8=A8=BC=E6=83=85=E5=A0=B1=E3=81=AE=E9=A0=85?= =?UTF-8?q?=E7=9B=AE=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Setting/SettingCommon.cs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/OpenTween/Setting/SettingCommon.cs b/OpenTween/Setting/SettingCommon.cs index f3a891ef7..77ac07246 100644 --- a/OpenTween/Setting/SettingCommon.cs +++ b/OpenTween/Setting/SettingCommon.cs @@ -73,6 +73,11 @@ public void Save(string settingsPath) [XmlIgnore] public UserAccount PrimaryAccount => this.UserAccounts.FirstOrDefault(x => x.Primary); + public MastodonCredential[] MastodonAccounts { get; set; } = new MastodonCredential[0]; + + [XmlIgnore] + public MastodonCredential MastodonPrimaryAccount => this.MastodonAccounts.FirstOrDefault(x => x.Primary); + public long UserId = 0; public string UserName = ""; public string Token = ""; @@ -332,4 +337,24 @@ public string AccessSecretPlain public override string ToString() => this.Username; } -} \ No newline at end of file + + public class MastodonCredential + { + public bool Primary { get; set; } + + public string InstanceUri { get; set; } = ""; + + public string Username { get; set; } = ""; + + public long UserId { get; set; } + + public string AccessTokenEncrypted { get; set; } = ""; + + [XmlIgnore] + public string AccessTokenPlain + { + get => MyCommon.IsNullOrEmpty(this.AccessTokenEncrypted) ? "" : MyCommon.DecryptString(this.AccessTokenEncrypted); + set => this.AccessTokenEncrypted = MyCommon.IsNullOrEmpty(value) ? "" : MyCommon.EncryptString(value); + } + } +} From b1c7b9afd140585cb7f5fe43431070c1d563092f Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 6 May 2017 10:15:28 +0900 Subject: [PATCH 12/25] =?UTF-8?q?Mastodon=E3=81=AE=E3=83=9B=E3=83=BC?= =?UTF-8?q?=E3=83=A0=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8BMastodonHomeTab?= =?UTF-8?q?=E3=82=AF=E3=83=A9=E3=82=B9=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Api/MastodonApi.cs | 62 ++++++++++++ OpenTween/Mastodon.cs | 151 ++++++++++++++++++++++++++++ OpenTween/Models/MastodonHomeTab.cs | 74 ++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 OpenTween/Api/MastodonApi.cs create mode 100644 OpenTween/Mastodon.cs create mode 100644 OpenTween/Models/MastodonHomeTab.cs diff --git a/OpenTween/Api/MastodonApi.cs b/OpenTween/Api/MastodonApi.cs new file mode 100644 index 000000000..c905c1303 --- /dev/null +++ b/OpenTween/Api/MastodonApi.cs @@ -0,0 +1,62 @@ +// 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 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using OpenTween.Api.DataModel; +using OpenTween.Connection; + +namespace OpenTween.Api +{ + public sealed class MastodonApi : IDisposable + { + public IMastodonApiConnection Connection { get; } + + public MastodonApi(Uri instanceUri, string? accessToken) + { + this.Connection = new MastodonApiConnection(instanceUri, accessToken); + } + + public Task TimelinesHome(long? maxId = null, long? sinceId = null, int? limit = null) + { + var endpoint = new Uri("/api/v1/timelines/home", UriKind.Relative); + var param = new Dictionary(); + + if (maxId != null) + param["max_id"] = maxId.ToString(); + if (sinceId != null) + param["since_id"] = sinceId.ToString(); + if (limit != null) + param["limit"] = limit.ToString(); + + return this.Connection.GetAsync(endpoint, param); + } + + public void Dispose() + => this.Connection.Dispose(); + } +} diff --git a/OpenTween/Mastodon.cs b/OpenTween/Mastodon.cs new file mode 100644 index 000000000..c6f1c57e3 --- /dev/null +++ b/OpenTween/Mastodon.cs @@ -0,0 +1,151 @@ +// 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 + +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using OpenTween.Api; +using OpenTween.Api.DataModel; +using OpenTween.Models; + +namespace OpenTween +{ + public sealed class Mastodon : IDisposable + { + public long UserId { get; private set; } + + public string Username { get; private set; } = ""; + + public MastodonApi Api => this.ApiInternal ?? throw new WebApiException("Unauthorized"); + + internal MastodonApi ApiInternal = null!; + + public void Initialize(MastodonCredential account) + { + this.ApiInternal = new MastodonApi(new Uri(account.InstanceUri), account.AccessTokenPlain); + + this.UserId = account.UserId; + this.Username = account.Username; + } + + public async Task GetHomeTimelineAsync(MastodonHomeTab tab, bool backward) + { + MastodonStatus[] statuses; + if (backward) + { + statuses = await this.Api.TimelinesHome(maxId: tab.OldestId) + .ConfigureAwait(false); + } + else + { + statuses = await this.Api.TimelinesHome() + .ConfigureAwait(false); + } + + return statuses.Select(x => this.CreatePost(x)).ToArray(); + } + + public PostClass CreatePost(MastodonStatus status) + { + var post = new PostClass(); + post.StatusId = status.Id; // TODO: Twitterの status_id と衝突する + + if (status.Reblog != null) + { + var reblog = status.Reblog; + post.CreatedAt = this.ParseDateTime(reblog.CreatedAt); + post.RetweetedId = reblog.Id; + post.TextFromApi = Regex.Replace(reblog.Content, "<[^>]+>", ""); + post.Text = reblog.Content; + post.IsFav = reblog.Favourited ?? false; + + if (reblog.InReplyToId != null) + { + post.InReplyToStatusId = reblog.InReplyToId.Value; + post.InReplyToUserId = reblog.InReplyToAccountId!.Value; + post.InReplyToUser = reblog.Mentions.FirstOrDefault()?.Acct ?? ""; + } + + post.UserId = reblog.Account.Id; + post.ScreenName = reblog.Account.Acct; + post.Nickname = reblog.Account.DisplayName; + post.ImageUrl = reblog.Account.AvatarStatic; + post.IsProtect = !(reblog.Visibility == "public" || reblog.Visibility == "unlisted"); + + post.RetweetedBy = status.Account.Acct; + post.RetweetedByUserId = status.Account.Id; + post.IsMe = status.Account.Id == this.UserId; + } + else // status.Reblog == null + { + post.CreatedAt = this.ParseDateTime(status.CreatedAt); + post.TextFromApi = Regex.Replace(status.Content, "<[^>]+>", ""); + post.Text = status.Content; + post.IsFav = status.Favourited ?? false; + + if (status.InReplyToId != null) + { + post.InReplyToStatusId = status.InReplyToId.Value; + post.InReplyToUserId = status.InReplyToAccountId!.Value; + post.InReplyToUser = status.Mentions.FirstOrDefault()?.Acct ?? ""; + } + + post.UserId = status.Account.Id; + post.ScreenName = status.Account.Acct; + post.Nickname = status.Account.DisplayName; + post.ImageUrl = status.Account.AvatarStatic; + post.IsProtect = !(status.Visibility == "public" || status.Visibility == "unlisted"); + post.IsMe = status.Account.Id == this.UserId; + } + + post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); + post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); + post.AccessibleText = post.TextFromApi; + + post.QuoteStatusIds = new long[0]; + post.ExpandedUrls = new PostClass.ExpandedUrlInfo[0]; + + var application = status.Application; + if (application != null) + { + post.Source = application.Name; + post.SourceUri = application.Website != null ? new Uri(application.Website) : null; + } + else + { + post.Source = ""; + } + + return post; + } + + public DateTimeUtc ParseDateTime(string datetime) + => DateTimeUtc.Parse(datetime, DateTimeFormatInfo.InvariantInfo); + + public void Dispose() + => this.ApiInternal?.Dispose(); + } +} diff --git a/OpenTween/Models/MastodonHomeTab.cs b/OpenTween/Models/MastodonHomeTab.cs new file mode 100644 index 000000000..3e1790cef --- /dev/null +++ b/OpenTween/Models/MastodonHomeTab.cs @@ -0,0 +1,74 @@ +// 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 + +using System; +using System.Threading.Tasks; +using OpenTween.Setting; + +namespace OpenTween.Models +{ + public class MastodonHomeTab : InternalStorageTabModel + { + public override MyCommon.TabUsageType TabType + => MyCommon.TabUsageType.Undefined; + + public override bool IsPermanentTabType => false; + + private readonly Mastodon mastodon; + + public MastodonHomeTab(Mastodon mastodon, string tabName) + : base(tabName) + { + this.mastodon = mastodon; + this.UnreadManage = true; + } + + public async override Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress) + { + bool read; + if (!SettingManager.Instance.Common.UnreadManage) + read = true; + else + read = startup && SettingManager.Instance.Common.Read; + + progress.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText5, backward ? -1 : 1)); + + var posts = await this.mastodon.GetHomeTimelineAsync(this, backward) + .ConfigureAwait(false); + + long? minimumId = null; + foreach (var post in posts) + { + if (minimumId == null || minimumId.Value > post.StatusId) + minimumId = post.StatusId; + + this.AddPostQueue(post); + } + + if (minimumId != null && minimumId.Value < this.OldestId) + this.OldestId = minimumId.Value; + + progress.Report(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText1); + } + } +} From c1c8baea634256dc48229a7234dc737f293d8149 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 6 May 2017 10:15:54 +0900 Subject: [PATCH 13/25] =?UTF-8?q?SettingCommon.xml=E3=81=A7Mastodon?= =?UTF-8?q?=E3=81=AE=E8=AA=8D=E8=A8=BC=E6=83=85=E5=A0=B1=E3=81=8C=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B=E5=A0=B4?= =?UTF-8?q?=E5=90=88=E3=81=AF=E8=B5=B7=E5=8B=95=E6=99=82=E3=81=AB=E3=83=9B?= =?UTF-8?q?=E3=83=BC=E3=83=A0=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Tween.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 3a552a750..c51401845 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -130,6 +130,8 @@ public partial class TweenMain : OTBaseForm // twitter解析部 private readonly Twitter tw; + private Mastodon? mastodon; + // Growl呼び出し部 private readonly GrowlHelper gh = new(ApplicationSettings.ApplicationName); @@ -525,6 +527,13 @@ ThumbnailGenerator thumbGenerator var primaryAccount = this.settings.Common.PrimaryAccount; this.tw.Initialize(primaryAccount.AccessToken, primaryAccount.AccessSecretPlain, primaryAccount.Username, primaryAccount.UserId); + var mastodonAccount = this.settings.Common.MastodonPrimaryAccount; + if (mastodonAccount != null) + { + this.mastodon = new Mastodon(); + this.mastodon.Initialize(mastodonAccount); + } + this.initial = true; this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; @@ -691,6 +700,9 @@ ThumbnailGenerator thumbGenerator this.ApplyListViewIconSize(this.settings.Common.IconSize); // <<<<<<<<タブ関連>>>>>>> + if (this.mastodon != null) + this.statuses.AddTab(new MastodonHomeTab(this.mastodon, "Mastodon")); + foreach (var tab in this.statuses.Tabs) { if (!this.AddNewTab(tab, startup: true)) @@ -747,7 +759,11 @@ ThumbnailGenerator thumbGenerator // タイマー設定 - this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Home] = () => this.InvokeAsync(() => this.RefreshTabAsync()); + this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Home] = () => this.InvokeAsync(() => Task.WhenAll(new[] + { + this.RefreshTabAsync(), + this.RefreshTabAsync(), + })); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Mention] = () => this.InvokeAsync(() => this.RefreshTabAsync()); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Dm] = () => this.InvokeAsync(() => this.RefreshTabAsync()); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.PublicSearch] = () => this.InvokeAsync(() => this.RefreshTabAsync()); @@ -7965,6 +7981,7 @@ private async void TweenMain_Shown(object sender, EventArgs e) this.RefreshTabAsync(), this.RefreshTabAsync(), this.RefreshTabAsync(), + this.RefreshTabAsync(), }; if (this.settings.Common.StartupFollowers) From 4c68b8d6904146b1134026cfd8baefbfaa5c076f Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 8 May 2017 22:57:37 +0900 Subject: [PATCH 14/25] =?UTF-8?q?Mastodon=E3=81=AE=E3=83=88=E3=82=A5?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AE=E3=81=B5=E3=81=81=E3=81=BC=E3=83=BB?= =?UTF-8?q?=E3=83=96=E3=83=BC=E3=82=B9=E3=83=88=E3=83=BB=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Api/MastodonApi.cs | 35 ++++++++++++ OpenTween/Mastodon.cs | 2 +- OpenTween/Models/MastodonPost.cs | 96 ++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 OpenTween/Models/MastodonPost.cs diff --git a/OpenTween/Api/MastodonApi.cs b/OpenTween/Api/MastodonApi.cs index c905c1303..eef28dac8 100644 --- a/OpenTween/Api/MastodonApi.cs +++ b/OpenTween/Api/MastodonApi.cs @@ -56,6 +56,41 @@ public Task TimelinesHome(long? maxId = null, long? sinceId = return this.Connection.GetAsync(endpoint, param); } + public Task StatusesDelete(long statusId) + { + var endpoint = new Uri($"/api/v1/statuses/{statusId}", UriKind.Relative); + + return this.Connection.PostLazyAsync(HttpMethod.Delete, endpoint, null).IgnoreResponse(); + } + + public Task> StatusesFavourite(long statusId) + { + var endpoint = new Uri($"/api/v1/statuses/{statusId}/favourite", UriKind.Relative); + + return this.Connection.PostLazyAsync(endpoint, null); + } + + public Task> StatusesUnfavourite(long statusId) + { + var endpoint = new Uri($"/api/v1/statuses/{statusId}/unfavourite", UriKind.Relative); + + return this.Connection.PostLazyAsync(endpoint, null); + } + + public Task> StatusesReblog(long statusId) + { + var endpoint = new Uri($"/api/v1/statuses/{statusId}/reblog", UriKind.Relative); + + return this.Connection.PostLazyAsync(endpoint, null); + } + + public Task> StatusesUnreblog(long statusId) + { + var endpoint = new Uri($"/api/v1/statuses/{statusId}/unreblog", UriKind.Relative); + + return this.Connection.PostLazyAsync(endpoint, null); + } + public void Dispose() => this.Connection.Dispose(); } diff --git a/OpenTween/Mastodon.cs b/OpenTween/Mastodon.cs index c6f1c57e3..e4319bcd0 100644 --- a/OpenTween/Mastodon.cs +++ b/OpenTween/Mastodon.cs @@ -70,7 +70,7 @@ public async Task GetHomeTimelineAsync(MastodonHomeTab tab, bool ba public PostClass CreatePost(MastodonStatus status) { - var post = new PostClass(); + var post = new MastodonPost(this); post.StatusId = status.Id; // TODO: Twitterの status_id と衝突する if (status.Reblog != null) diff --git a/OpenTween/Models/MastodonPost.cs b/OpenTween/Models/MastodonPost.cs new file mode 100644 index 000000000..d2ff83538 --- /dev/null +++ b/OpenTween/Models/MastodonPost.cs @@ -0,0 +1,96 @@ +// 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 + +using System; +using System.Threading.Tasks; +using OpenTween.Connection; + +namespace OpenTween.Models +{ + public class MastodonPost : PostClass + { + private readonly Mastodon mastodon; + + public MastodonPost(Mastodon mastodon) + => this.mastodon = mastodon; + + public override async Task FavoriteAsync(SettingCommon settingCommon) + { + var statusId = this.IsRetweet ? this.RetweetedId : this.StatusId; + + await this.mastodon.Api.StatusesFavourite(statusId) + .IgnoreResponse() + .ConfigureAwait(false); + + this.IsFav = true; + } + + public override async Task UnfavoriteAsync() + { + var statusId = this.IsRetweet ? this.RetweetedId : this.StatusId; + + await this.mastodon.Api.StatusesUnfavourite(statusId) + .IgnoreResponse() + .ConfigureAwait(false); + + this.IsFav = false; + } + + public override async Task RetweetAsync(SettingCommon settingCommon) + { + var statusId = this.IsRetweet ? this.RetweetedId : this.StatusId; + + var response = await this.mastodon.Api.StatusesReblog(statusId) + .ConfigureAwait(false); + + var status = await response.LoadJsonAsync() + .ConfigureAwait(false); + + return this.mastodon.CreatePost(status); + } + + public override Task DeleteAsync() + { + if (this.IsRetweet && this.RetweetedByUserId == this.mastodon.UserId) + { + // 自分がブーストしたトゥート (自分がブーストした自分のトゥートも含む) + // => ブーストを取り消し + return this.mastodon.Api.StatusesUnreblog(this.StatusId); + } + else + { + if (this.UserId != this.mastodon.UserId) + throw new InvalidOperationException(); + + if (this.IsRetweet) + // 他人にブーストされた自分のトゥート + // => ブースト元の自分のトゥートを削除 + return this.mastodon.Api.StatusesDelete(this.RetweetedId); + else + // 自分のトゥート + // => トゥートを削除 + return this.mastodon.Api.StatusesDelete(this.StatusId); + } + } + } +} From 88c381974a58077710b91b7e66ea42c408f85a69 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Wed, 10 May 2017 02:13:26 +0900 Subject: [PATCH 15/25] =?UTF-8?q?Mastodon=E3=81=B8=E3=81=AE=E3=83=88?= =?UTF-8?q?=E3=82=A5=E3=83=BC=E3=83=88=E3=81=AE=E6=8A=95=E7=A8=BF=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 画像アップロードには未対応 --- OpenTween/Api/MastodonApi.cs | 31 +++++++++++++++++++++++++++++++ OpenTween/Mastodon.cs | 11 +++++++++++ OpenTween/PostStatusParams.cs | 2 ++ OpenTween/Tween.cs | 26 ++++++++++++++++++++++---- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/OpenTween/Api/MastodonApi.cs b/OpenTween/Api/MastodonApi.cs index eef28dac8..fd752ab72 100644 --- a/OpenTween/Api/MastodonApi.cs +++ b/OpenTween/Api/MastodonApi.cs @@ -56,6 +56,37 @@ public Task TimelinesHome(long? maxId = null, long? sinceId = return this.Connection.GetAsync(endpoint, param); } + public Task> StatusesPost( + string status, + long? inReplyToId = null, + IReadOnlyList? mediaIds = null, + bool? sensitive = null, + string? spoilerText = null, + string? visibility = null) + { + var endpoint = new Uri("/api/v1/statuses", UriKind.Relative); + var param = new Dictionary + { + ["status"] = status, + }; + + if (inReplyToId != null) + param["in_reply_to_id"] = inReplyToId.ToString(); + if (sensitive != null) + param["sensitive"] = sensitive.Value ? "true" : "false"; + if (spoilerText != null) + param["spoiler_text"] = spoilerText; + if (visibility != null) + param["visibility"] = visibility; + + var paramList = param.ToList(); + + foreach (var mediaId in mediaIds ?? Enumerable.Empty()) + paramList.Add(new KeyValuePair("media_ids[]", mediaId.ToString())); + + return this.Connection.PostLazyAsync(endpoint, paramList); + } + public Task StatusesDelete(long statusId) { var endpoint = new Uri($"/api/v1/statuses/{statusId}", UriKind.Relative); diff --git a/OpenTween/Mastodon.cs b/OpenTween/Mastodon.cs index e4319bcd0..458ddf472 100644 --- a/OpenTween/Mastodon.cs +++ b/OpenTween/Mastodon.cs @@ -51,6 +51,17 @@ public void Initialize(MastodonCredential account) this.Username = account.Username; } + public async Task PostStatusAsync(PostStatusParams param) + { + var response = await this.Api.StatusesPost(param.Text, param.InReplyToStatusId, param.MediaIds) + .ConfigureAwait(false); + + var status = await response.LoadJsonAsync() + .ConfigureAwait(false); + + return this.CreatePost(status); + } + public async Task GetHomeTimelineAsync(MastodonHomeTab tab, bool backward) { MastodonStatus[] statuses; diff --git a/OpenTween/PostStatusParams.cs b/OpenTween/PostStatusParams.cs index 971e226d9..f1ed94f58 100644 --- a/OpenTween/PostStatusParams.cs +++ b/OpenTween/PostStatusParams.cs @@ -43,5 +43,7 @@ public class PostStatusParams public IReadOnlyList ExcludeReplyUserIds { get; set; } = Array.Empty(); public string? AttachmentUrl { get; set; } + + public bool PostToMastodon { get; set; } = false; } } diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index c51401845..12ad2722c 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -1318,6 +1318,9 @@ private async void PostButton_Click(object sender, EventArgs e) await MyCommon.OpenInBrowserAsync(this, tmp); } + // 表示中のタブが Mastodon であれば投稿先を Mastodon にする + status.PostToMastodon = this.CurrentTab is MastodonHomeTab; + await this.PostMessageAsync(status, uploadService, uploadItems); } @@ -1692,8 +1695,16 @@ await Task.Run(async () => .ConfigureAwait(false); } - post = await this.tw.PostStatus(postParamsWithMedia) - .ConfigureAwait(false); + if (postParams.PostToMastodon) + { + post = await this.mastodon.PostStatusAsync(postParamsWithMedia) + .ConfigureAwait(false); + } + else + { + post = await this.tw.PostStatus(postParamsWithMedia) + .ConfigureAwait(false); + } }); p.Report(Properties.Resources.PostWorker_RunWorkerCompletedText4); @@ -1777,8 +1788,15 @@ await Task.Run(async () => // TLに反映 if (post != null) { - this.statuses.AddPost(post); - this.statuses.DistributePosts(); + if (postParams.PostToMastodon) + { + this.statuses.GetTabByType()!.AddPostQueue(post); + } + else + { + this.statuses.AddPost(post); + this.statuses.DistributePosts(); + } this.RefreshTimeline(); } From 0f26a3ba013ab12973d9eb9b44cba8ea8e454d8d Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Fri, 12 May 2017 00:01:24 +0900 Subject: [PATCH 16/25] =?UTF-8?q?=E3=83=88=E3=82=A5=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=AB=E6=B7=BB=E4=BB=98=E3=81=95=E3=82=8C=E3=81=9F=E7=94=BB?= =?UTF-8?q?=E5=83=8F=E3=81=AE=E3=82=B5=E3=83=A0=E3=83=8D=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Mastodon.cs | 6 ++++++ OpenTween/Thumbnail/ThumbnailGenerator.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OpenTween/Mastodon.cs b/OpenTween/Mastodon.cs index 458ddf472..9afc09646 100644 --- a/OpenTween/Mastodon.cs +++ b/OpenTween/Mastodon.cs @@ -100,6 +100,9 @@ public PostClass CreatePost(MastodonStatus status) post.InReplyToUser = reblog.Mentions.FirstOrDefault()?.Acct ?? ""; } + post.Media = reblog.MediaAttachments + .Select(x => new MediaInfo(x.PreviewUrl)).ToList(); + post.UserId = reblog.Account.Id; post.ScreenName = reblog.Account.Acct; post.Nickname = reblog.Account.DisplayName; @@ -124,6 +127,9 @@ public PostClass CreatePost(MastodonStatus status) post.InReplyToUser = status.Mentions.FirstOrDefault()?.Acct ?? ""; } + post.Media = status.MediaAttachments + .Select(x => new MediaInfo(x.PreviewUrl)).ToList(); + post.UserId = status.Account.Id; post.ScreenName = status.Account.Acct; post.Nickname = status.Account.DisplayName; diff --git a/OpenTween/Thumbnail/ThumbnailGenerator.cs b/OpenTween/Thumbnail/ThumbnailGenerator.cs index 033eb35b5..e5aac599f 100644 --- a/OpenTween/Thumbnail/ThumbnailGenerator.cs +++ b/OpenTween/Thumbnail/ThumbnailGenerator.cs @@ -70,7 +70,7 @@ public ThumbnailGenerator(ImgAzyobuziNet imgAzyobuziNet) new Vimeo(), // DirectLink - new SimpleThumbnailService(@"^https?://.*(\.jpg|\.jpeg|\.gif|\.png|\.bmp)$", "${0}"), + new SimpleThumbnailService(@"^https?://.*(\.jpg|\.jpeg|\.gif|\.png|\.bmp)(\?[^#]+)?(#.+)?$", "${0}"), // img.azyobuzi.net this.ImgAzyobuziNet, From 19c737528646784d0f7843055acaba6c41d4b027 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 14 May 2017 21:07:34 +0900 Subject: [PATCH 17/25] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=ABMastodon=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AB=E9=96=A2=E3=81=99=E3=82=8B=E3=82=B3=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Setting/Panel/BasedPanel.Designer.cs | 27 +++++++++++++++ OpenTween/Setting/Panel/BasedPanel.cs | 4 +++ OpenTween/Setting/Panel/BasedPanel.en.resx | 3 ++ OpenTween/Setting/Panel/BasedPanel.resx | 33 +++++++++++++++++-- 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/OpenTween/Setting/Panel/BasedPanel.Designer.cs b/OpenTween/Setting/Panel/BasedPanel.Designer.cs index 88c42afc9..f3f85dafd 100644 --- a/OpenTween/Setting/Panel/BasedPanel.Designer.cs +++ b/OpenTween/Setting/Panel/BasedPanel.Designer.cs @@ -35,6 +35,9 @@ private void InitializeComponent() this.AuthClearButton = new System.Windows.Forms.Button(); this.Label4 = new System.Windows.Forms.Label(); this.panel1 = new System.Windows.Forms.Panel(); + this.buttonMastodonAuth = new System.Windows.Forms.Button(); + this.labelMastodonAccount = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); this.panel1.SuspendLayout(); this.SuspendLayout(); // @@ -76,9 +79,30 @@ private void InitializeComponent() this.panel1.Controls.Add(this.StartAuthButton); this.panel1.Controls.Add(this.AuthClearButton); this.panel1.Controls.Add(this.Label4); + this.panel1.Controls.Add(this.buttonMastodonAuth); + this.panel1.Controls.Add(this.labelMastodonAccount); + this.panel1.Controls.Add(this.label1); resources.ApplyResources(this.panel1, "panel1"); this.panel1.Name = "panel1"; // + // buttonMastodonAuth + // + resources.ApplyResources(this.buttonMastodonAuth, "buttonMastodonAuth"); + this.buttonMastodonAuth.Name = "buttonMastodonAuth"; + this.buttonMastodonAuth.UseVisualStyleBackColor = true; + this.buttonMastodonAuth.Click += new System.EventHandler(this.ButtonMastodonAuth_Click); + // + // labelMastodonAccount + // + this.labelMastodonAccount.AutoEllipsis = true; + resources.ApplyResources(this.labelMastodonAccount, "labelMastodonAccount"); + this.labelMastodonAccount.Name = "labelMastodonAccount"; + // + // label1 + // + resources.ApplyResources(this.label1, "label1"); + this.label1.Name = "label1"; + // // BasedPanel // resources.ApplyResources(this, "$this"); @@ -99,5 +123,8 @@ private void InitializeComponent() internal System.Windows.Forms.Button AuthClearButton; internal System.Windows.Forms.Label Label4; private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.Button buttonMastodonAuth; + private System.Windows.Forms.Label labelMastodonAccount; + private System.Windows.Forms.Label label1; } } diff --git a/OpenTween/Setting/Panel/BasedPanel.cs b/OpenTween/Setting/Panel/BasedPanel.cs index f5ba44543..646ff18f8 100644 --- a/OpenTween/Setting/Panel/BasedPanel.cs +++ b/OpenTween/Setting/Panel/BasedPanel.cs @@ -99,5 +99,9 @@ private void AuthClearButton_Click(object sender, EventArgs e) } } } + + private void ButtonMastodonAuth_Click(object sender, EventArgs e) + { + } } } diff --git a/OpenTween/Setting/Panel/BasedPanel.en.resx b/OpenTween/Setting/Panel/BasedPanel.en.resx index 6eb8d1ac7..bda2d2856 100644 --- a/OpenTween/Setting/Panel/BasedPanel.en.resx +++ b/OpenTween/Setting/Panel/BasedPanel.en.resx @@ -6,7 +6,10 @@ Remove + Authorize Sign up for Twitter account + 100, 12 + Mastodon Account 47, 12 Account Start Authentication diff --git a/OpenTween/Setting/Panel/BasedPanel.resx b/OpenTween/Setting/Panel/BasedPanel.resx index 7d65746a2..2badefbfa 100644 --- a/OpenTween/Setting/Panel/BasedPanel.resx +++ b/OpenTween/Setting/Panel/BasedPanel.resx @@ -11,7 +11,7 @@ True 0, 0, 0, 0 BasedPanel - OpenTween.Setting.Panel.SettingPanelBase, OpenTween, Version=2.4.3.1, Culture=neutral, PublicKeyToken=null + OpenTween.Setting.Panel.SettingPanelBase, OpenTween, Version=2.7.1.1, Culture=neutral, PublicKeyToken=null AuthClearButton panel1 System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -20,14 +20,26 @@ panel1 System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 0 + buttonMastodonAuth + panel1 + System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 5 CreateAccountButton panel1 System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 1 + label1 + panel1 + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 7 Label4 panel1 System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 4 + labelMastodonAccount + panel1 + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 6 panel1 $this System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -45,19 +57,36 @@ 111, 22 160, 20 1 + NoControl + 401, 182 + 75, 23 + 17 + 認証 Bottom, Right True NoControl - 310, 320 + 310, 123 186, 25 4 Twitter アカウントを作成する + True + NoControl + 23, 187 + 98, 12 + 15 + Mastodonアカウント True NoControl 23, 25 49, 12 0 アカウント + NoControl + 138, 182 + 257, 23 + 16 + labelMastodonAccount + MiddleLeft Fill 0, 0 20, 20, 20, 20 From 440d47d8d9fa3675438be143aa60fe84eda83e7d Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Tue, 16 May 2017 21:37:43 +0900 Subject: [PATCH 18/25] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=ABMastodon=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AE=E8=AA=8D=E8=A8=BC=E6=A9=9F=E8=83=BD=E3=82=92=E5=AE=9F?= =?UTF-8?q?=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 | 88 +++++++++++++++++++ OpenTween/AppendSettingDialog.cs | 9 +- OpenTween/Mastodon.cs | 52 +++++++++++ OpenTween/Setting/Panel/BasedPanel.cs | 60 ++++++++++++- OpenTween/Tween.cs | 31 ++++++- 7 files changed, 318 insertions(+), 8 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 fd752ab72..91d5aba02 100644 --- a/OpenTween/Api/MastodonApi.cs +++ b/OpenTween/Api/MastodonApi.cs @@ -36,9 +36,97 @@ 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/AppendSettingDialog.cs b/OpenTween/AppendSettingDialog.cs index b2d79f0c5..89f0bfa00 100644 --- a/OpenTween/AppendSettingDialog.cs +++ b/OpenTween/AppendSettingDialog.cs @@ -252,8 +252,7 @@ public void ApplyNetworkSettings() var pinPageUrl = TwitterApiConnection.GetAuthorizeUri(requestToken); - var browserPath = this.ActionPanel.BrowserPathText.Text; - var pin = AuthDialog.DoAuth(this, pinPageUrl, browserPath); + var pin = this.ShowAuthDialog(pinPageUrl); if (MyCommon.IsNullOrEmpty(pin)) return null; // キャンセルされた場合 @@ -268,6 +267,12 @@ public void ApplyNetworkSettings() }; } + public string? ShowAuthDialog(Uri pinPageUrl) + { + var browserPath = this.ActionPanel.BrowserPathText.Text; + return AuthDialog.DoAuth(this, pinPageUrl, browserPath); + } + private void CheckPostAndGet_CheckedChanged(object sender, EventArgs e) => this.GetPeriodPanel.LabelPostAndGet.Visible = this.GetPeriodPanel.CheckPostAndGet.Checked; diff --git a/OpenTween/Mastodon.cs b/OpenTween/Mastodon.cs index 9afc09646..d9ecf6c35 100644 --- a/OpenTween/Mastodon.cs +++ b/OpenTween/Mastodon.cs @@ -51,6 +51,58 @@ 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/Setting/Panel/BasedPanel.cs b/OpenTween/Setting/Panel/BasedPanel.cs index 646ff18f8..b508f88c9 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) @@ -82,6 +90,25 @@ public void SaveConfig(SettingCommon settingCommon) settingCommon.Token = ""; settingCommon.TokenSecret = ""; } + + var mastodonCredential = this.mastodonCredential; + if (mastodonCredential != null) + { + mastodonCredential.Primary = true; + settingCommon.MastodonAccounts = new[] { mastodonCredential }; + } + else + { + settingCommon.MastodonAccounts = new MastodonCredential[0]; + } + } + + private void RefreshMastodonCredential() + { + if (this.mastodonCredential != null) + this.labelMastodonAccount.Text = this.mastodonCredential.Username; + else + this.labelMastodonAccount.Text = "(未設定)"; } private void AuthClearButton_Click(object sender, EventArgs e) @@ -100,8 +127,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 = ((AppendSettingDialog)this.ParentForm).ShowAuthDialog(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 12ad2722c..f46e37285 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -130,7 +130,7 @@ public partial class TweenMain : OTBaseForm // twitter解析部 private readonly Twitter tw; - private Mastodon? mastodon; + private Mastodon mastodon = new Mastodon(); // Growl呼び出し部 private readonly GrowlHelper gh = new(ApplicationSettings.ApplicationName); @@ -700,15 +700,14 @@ ThumbnailGenerator thumbGenerator this.ApplyListViewIconSize(this.settings.Common.IconSize); // <<<<<<<<タブ関連>>>>>>> - if (this.mastodon != null) - this.statuses.AddTab(new MastodonHomeTab(this.mastodon, "Mastodon")); - foreach (var tab in this.statuses.Tabs) { if (!this.AddNewTab(tab, startup: true)) throw new TabException(Properties.Resources.TweenMain_LoadText1); } + this.ReloadMastodonHomeTab(startup: true); + this.ListTabSelect(this.ListTab.SelectedTab); // タブの位置を調整する @@ -2537,10 +2536,24 @@ private DialogResult ShowSettingDialog() 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 previousUserId = this.settings.Common.UserId; + var oldMastodonUser = this.settings.Common.MastodonPrimaryAccount; var oldIconCol = this.Use2ColumnsMode; if (this.ShowSettingDialog() == DialogResult.OK) @@ -2560,6 +2573,13 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) this.tw.Initialize("", "", "", 0); } + var primaryMastodonAccount = this.settings.Common.MastodonPrimaryAccount; + if (primaryMastodonAccount != null && primaryMastodonAccount != oldMastodonUser) + { + this.mastodon.Initialize(primaryMastodonAccount); + this.ReloadMastodonHomeTab(); + } + this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; @@ -2747,6 +2767,9 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) if (this.tw.UserId != previousUserId) await this.DoGetFollowersMenu(); + + if (this.settings.Common.MastodonPrimaryAccount != oldMastodonUser) + await this.RefreshTabAsync(); } /// From b3997c9dc426dfa58e78358e25d28200b5ff05ed Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Thu, 18 May 2017 01:48:51 +0900 Subject: [PATCH 19/25] =?UTF-8?q?=E4=BA=8B=E5=89=8D=E3=81=AB=E7=99=BA?= =?UTF-8?q?=E8=A1=8C=E3=81=97=E3=81=9FMastodon=E3=81=AEclient=5Fid,=20clie?= =?UTF-8?q?nt=5Fsecret=E3=81=AE=E7=B5=84=E3=82=92ApplicetionSettings?= =?UTF-8?q?=E5=86=85=E3=81=AB=E8=A8=98=E8=BF=B0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/ApplicationSettings.cs | 14 ++++++++++++++ OpenTween/Mastodon.cs | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/OpenTween/ApplicationSettings.cs b/OpenTween/ApplicationSettings.cs index d7c7a466d..9111c35b2 100644 --- a/OpenTween/ApplicationSettings.cs +++ b/OpenTween/ApplicationSettings.cs @@ -127,6 +127,20 @@ internal static class ApplicationSettings /// public static readonly ApiKey TwitterConsumerSecret = ApiKey.Create("%e%p93BdDzlwbYIC5Ych/47OQ==%xYZTCYaBxzS4An3o7Qcigjp9QMtu5vi5iEAW/sNgoOoAUyuHJRPP3Ovs20ZV2fAYKxUDiu76dxLfObwI7QjSRA==%YEruRDAQdbJzO+y6kn7+U/uIyIyNra/8Ulo+L6KJcWA="); + // ===================================================================== + // Mastodon + + /// + /// Mastodon インスタンス毎に事前に発行した client_id, client_secret の組 + /// + /// + /// ここに含まれていないインスタンスでは によって + /// アプリケーションの登録を都度行います + /// + public static readonly IReadOnlyDictionary> MastodonClientIds = new Dictionary> + { + }; + // ===================================================================== // Foursquare // https://developer.foursquare.com/ から取得できます。 diff --git a/OpenTween/Mastodon.cs b/OpenTween/Mastodon.cs index d9ecf6c35..090b5a581 100644 --- a/OpenTween/Mastodon.cs +++ b/OpenTween/Mastodon.cs @@ -53,6 +53,15 @@ public void Initialize(MastodonCredential account) public static async Task RegisterClientAsync(Uri instanceUri) { + if (ApplicationSettings.MastodonClientIds.TryGetValue(instanceUri.Host, out var client)) + { + return new MastodonRegisteredApp + { + ClientId = client.Item1, + ClientSecret = client.Item2, + }; + } + using var api = new MastodonApi(instanceUri); var redirectUri = new Uri("urn:ietf:wg:oauth:2.0:oob"); var scope = "read write follow"; From 12f475d4db47a0b4f7d1309be2a67f1607fd19b6 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 7 Mar 2020 09:18:21 +0900 Subject: [PATCH 20/25] =?UTF-8?q?Twitter=E3=82=A2=E3=82=AB=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=88=E6=9C=AA=E8=AA=8D=E8=A8=BC=E3=81=AE=E7=8A=B6?= =?UTF-8?q?=E6=85=8B=E3=81=A7=E3=82=82=E8=B5=B7=E5=8B=95=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BB=AE=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/AppendSettingDialog.cs | 2 +- OpenTween/Setting/Panel/BasedPanel.cs | 2 ++ OpenTween/Setting/SettingManager.cs | 2 +- OpenTween/Tween.cs | 23 +++++------------------ 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/OpenTween/AppendSettingDialog.cs b/OpenTween/AppendSettingDialog.cs index 89f0bfa00..af88f27b4 100644 --- a/OpenTween/AppendSettingDialog.cs +++ b/OpenTween/AppendSettingDialog.cs @@ -121,7 +121,7 @@ private void Setting_FormClosing(object sender, FormClosingEventArgs e) { if (MyCommon.EndingFlag) return; - if (this.BasedPanel.AuthUserCombo.SelectedIndex == -1 && e.CloseReason == CloseReason.None) + if (this.BasedPanel.AuthUserCombo.SelectedIndex == -1 && !this.BasedPanel.HasMastodonCredential && e.CloseReason == CloseReason.None) { if (MessageBox.Show(Properties.Resources.Setting_FormClosing1, "Confirm", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel) { diff --git a/OpenTween/Setting/Panel/BasedPanel.cs b/OpenTween/Setting/Panel/BasedPanel.cs index b508f88c9..7e507cb2a 100644 --- a/OpenTween/Setting/Panel/BasedPanel.cs +++ b/OpenTween/Setting/Panel/BasedPanel.cs @@ -40,6 +40,8 @@ namespace OpenTween.Setting.Panel { public partial class BasedPanel : SettingPanelBase { + public bool HasMastodonCredential => this.mastodonCredential != null; + private MastodonCredential? mastodonCredential = null; public BasedPanel() diff --git a/OpenTween/Setting/SettingManager.cs b/OpenTween/Setting/SettingManager.cs index 431ab6a3b..d7e8bead2 100644 --- a/OpenTween/Setting/SettingManager.cs +++ b/OpenTween/Setting/SettingManager.cs @@ -49,7 +49,7 @@ public class SettingManager /// ユーザによる設定が必要な項目が残っているか public bool IsIncomplete - => this.Common.PrimaryAccount == null; + => this.Common.PrimaryAccount == null && this.Common.MastodonPrimaryAccount == null; public bool IsFirstRun { get; private set; } = false; diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index f46e37285..85d2b9f8e 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -524,8 +524,11 @@ ThumbnailGenerator thumbGenerator var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); // 認証関連 - var primaryAccount = this.settings.Common.PrimaryAccount; - this.tw.Initialize(primaryAccount.AccessToken, primaryAccount.AccessSecretPlain, primaryAccount.Username, primaryAccount.UserId); + var twitterAccount = this.settings.Common.PrimaryAccount; + if (twitterAccount != null) + this.tw.Initialize(twitterAccount.AccessToken, twitterAccount.AccessSecretPlain, twitterAccount.Username, twitterAccount.UserId); + else + this.tw.Initialize("", "", "", 0L); var mastodonAccount = this.settings.Common.MastodonPrimaryAccount; if (mastodonAccount != null) @@ -539,22 +542,6 @@ ThumbnailGenerator thumbGenerator this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; - // アクセストークンが有効であるか確認する - // ここが Twitter API への最初のアクセスになるようにすること - try - { - this.tw.VerifyCredentials(); - } - catch (WebApiException ex) - { - MessageBox.Show( - this, - string.Format(Properties.Resources.StartupAuthError_Text, ex.Message), - ApplicationSettings.ApplicationName, - MessageBoxButtons.OK, - MessageBoxIcon.Warning); - } - // サムネイル関連の初期化 // プロキシ設定等の通信まわりの初期化が済んでから処理する var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet; From e7a9c8987e9499d5f58a9f62bd1f454b6097497b Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 7 Mar 2020 20:32:24 +0900 Subject: [PATCH 21/25] =?UTF-8?q?Mastodon=E5=AF=BE=E5=BF=9C=E7=89=88?= =?UTF-8?q?=E3=81=AE=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E8=A1=A8?= =?UTF-8?q?=E8=A8=98=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/MyCommonTest.cs | 14 +++++++------- OpenTween/MyCommon.cs | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/OpenTween.Tests/MyCommonTest.cs b/OpenTween.Tests/MyCommonTest.cs index 45e286251..4e9cd0c20 100644 --- a/OpenTween.Tests/MyCommonTest.cs +++ b/OpenTween.Tests/MyCommonTest.cs @@ -176,13 +176,13 @@ public void ReplaceAppNameTest(string str, string excepted) => Assert.Equal(excepted, MyCommon.ReplaceAppName(str, "OpenTween")); [Theory] - [InlineData("1.0.0.0", "1.0.0")] - [InlineData("1.0.0.1", "1.0.1-dev")] - [InlineData("1.0.0.12", "1.0.1-dev+build.12")] - [InlineData("1.0.1.0", "1.0.1")] - [InlineData("1.0.9.1", "1.0.10-dev")] - [InlineData("1.1.0.0", "1.1.0")] - [InlineData("1.9.9.1", "1.9.10-dev")] + [InlineData("1.0.0.0", "1.0.0+mastodon")] + [InlineData("1.0.0.1", "1.0.1-dev+mastodon")] + [InlineData("1.0.0.12", "1.0.1-dev+mastodon+build.12")] + [InlineData("1.0.1.0", "1.0.1+mastodon")] + [InlineData("1.0.9.1", "1.0.10-dev+mastodon")] + [InlineData("1.1.0.0", "1.1.0+mastodon")] + [InlineData("1.9.9.1", "1.9.10-dev+mastodon")] public void GetReadableVersionTest(string fileVersion, string expected) => Assert.Equal(expected, MyCommon.GetReadableVersion(fileVersion)); diff --git a/OpenTween/MyCommon.cs b/OpenTween/MyCommon.cs index 6ac6d05eb..6deecc757 100644 --- a/OpenTween/MyCommon.cs +++ b/OpenTween/MyCommon.cs @@ -828,16 +828,16 @@ public static string GetReadableVersion(Version version) if (versionNum[3] == 0) { - return string.Format("{0}.{1}.{2}", versionNum[0], versionNum[1], versionNum[2]); + return string.Format("{0}.{1}.{2}+mastodon", versionNum[0], versionNum[1], versionNum[2]); } else { versionNum[2] = versionNum[2] + 1; if (versionNum[3] == 1) - return string.Format("{0}.{1}.{2}-dev", versionNum[0], versionNum[1], versionNum[2]); + return string.Format("{0}.{1}.{2}-dev+mastodon", versionNum[0], versionNum[1], versionNum[2]); else - return string.Format("{0}.{1}.{2}-dev+build.{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]); + return string.Format("{0}.{1}.{2}-dev+mastodon+build.{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]); } } From 3c8aea6d45e0c00e20b9ae87d5939048219c8141 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 7 Mar 2020 20:33:54 +0900 Subject: [PATCH 22/25] =?UTF-8?q?Mastodon=E5=AF=BE=E5=BF=9C=E3=81=AE?= =?UTF-8?q?=E9=96=8B=E7=99=BA=E7=89=88=E3=81=A7=E3=81=AF=E3=82=A2=E3=83=83?= =?UTF-8?q?=E3=83=97=E3=83=87=E3=83=BC=E3=83=88=E7=A2=BA=E8=AA=8D=E3=82=92?= =?UTF-8?q?=E7=84=A1=E5=8A=B9=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/ApplicationSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenTween/ApplicationSettings.cs b/OpenTween/ApplicationSettings.cs index 9111c35b2..a148f72f4 100644 --- a/OpenTween/ApplicationSettings.cs +++ b/OpenTween/ApplicationSettings.cs @@ -103,7 +103,7 @@ internal static class ApplicationSettings /// version.txt のフォーマットについては http://sourceforge.jp/projects/opentween/wiki/VersionTxt を参照。 /// 派生プロジェクトなどでこの機能を無効にする場合は null をセットして下さい。 /// - public static readonly string VersionInfoUrl = "https://www.opentween.org/status/version.txt"; + public static readonly string? VersionInfoUrl = null; // ===================================================================== // 暗号化キー From 831b3c42aa3a927169cf800ed24cbe227e5bac54 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 7 Mar 2020 20:35:41 +0900 Subject: [PATCH 23/25] =?UTF-8?q?Twitter=E6=9C=AA=E8=AA=8D=E8=A8=BC?= =?UTF-8?q?=E3=81=8B=E3=81=A4Mastodon=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=8C=E6=9C=89=E5=8A=B9=E3=81=AA=E5=A0=B4=E5=90=88?= =?UTF-8?q?=E3=81=AFMastodon=E3=82=BF=E3=83=96=E3=82=92=E8=B5=B7=E5=8B=95?= =?UTF-8?q?=E6=99=82=E3=81=AB=E9=81=B8=E6=8A=9E=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Tween.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 85d2b9f8e..522209fb3 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -697,6 +697,16 @@ ThumbnailGenerator thumbGenerator this.ListTabSelect(this.ListTab.SelectedTab); + TabModel firstSelectedTab; + if (this.settings.Common.PrimaryAccount == null && this.settings.Common.MastodonPrimaryAccount != null) + firstSelectedTab = this.statuses.GetTabByType()!; + else + firstSelectedTab = this.statuses.Tabs[0]; + + var firstSelectedTabIndex = this.statuses.Tabs.IndexOf(firstSelectedTab); + this.ListTab.SelectedIndex = firstSelectedTabIndex; + this.ListTabSelect(this.ListTab.SelectedTab); + // タブの位置を調整する this.SetTabAlignment(); From 001d56cbc9ad1e81f16b1a62f0da4b7080698860 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Fri, 2 Dec 2022 05:10:14 +0900 Subject: [PATCH 24/25] =?UTF-8?q?Twitter.AccountState=E3=81=AE=E3=83=81?= =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E7=84=A1=E5=8A=B9=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Tween.cs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 522209fb3..67ddec2ac 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -1355,24 +1355,8 @@ private void NotifyIcon1_BalloonTipClicked(object sender, EventArgs e) this.BringToFront(); } - private static int errorCount = 0; - private static bool CheckAccountValid() - { - if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) - { - errorCount += 1; - if (errorCount > 5) - { - errorCount = 0; - Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid; - return true; - } - return false; - } - errorCount = 0; - return true; - } + => true; /// 指定された型 に合致する全てのタブを更新します private Task RefreshTabAsync() From 7a1f327896b224cf49e151f62926b2f6bf5b9c18 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Fri, 2 Dec 2022 05:28:34 +0900 Subject: [PATCH 25/25] =?UTF-8?q?GitHub=20Actions=E3=81=A7=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E5=A4=B1=E6=95=97=E6=99=82=E3=81=AE=E7=B5=82?= =?UTF-8?q?=E4=BA=86=E3=82=B3=E3=83=BC=E3=83=89=E3=81=8C=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=96=E3=81=AE=E6=88=90=E5=90=A6=E3=81=AB=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7de4ede0..ad475e3ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,7 @@ jobs: $altCoverPath = "$($env:NUGET_PACKAGES)\altcover\$($altCoverVersion)\tools\$($targetFramework)\AltCover.exe" $xunitPath = "$($env:NUGET_PACKAGES)\xunit.runner.console\$($xunitVersion)\tools\$($targetFramework)\xunit.console.exe" - Start-Process ` + $p = Start-Process ` -FilePath $altCoverPath ` -ArgumentList ( '--inputDirectory', @@ -105,9 +105,14 @@ jobs: '--visibleBranches' ) ` -NoNewWindow ` + -PassThru ` -Wait - Start-Process ` + if ($p.ExitCode -ne 0) { + exit $p.ExitCode + } + + $p = Start-Process ` -FilePath $altCoverPath ` -ArgumentList ( 'runner', @@ -119,8 +124,13 @@ jobs: '.\__Instrumented\OpenTween.Tests.dll' ) ` -NoNewWindow ` + -PassThru ` -Wait + if ($p.ExitCode -ne 0) { + exit $p.ExitCode + } + - name: Upload test results to codecov shell: pwsh run: |