diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b0cc0c5c..f668c7178 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1 + uses: microsoft/setup-msbuild@v2 - name: Set configuration env shell: pwsh @@ -30,7 +30,7 @@ jobs: echo 'CONFIGURATION=Debug' >> $env:GITHUB_ENV } - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ${{ github.workspace }}/.nuget/packages key: nuget-${{ hashFiles('*/*.csproj') }} diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index f55909257..7a5e40709 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -25,7 +25,7 @@ jobs: ref: '${{ github.event.pull_request.head.sha }}' - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1 + uses: microsoft/setup-msbuild@v2 - name: Set configuration env shell: pwsh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6aa217f6..71bbcda0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1 + uses: microsoft/setup-msbuild@v2 - name: Set configuration env shell: pwsh @@ -35,7 +35,7 @@ jobs: echo 'CONFIGURATION=Debug' >> $env:GITHUB_ENV } - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ${{ github.workspace }}/.nuget/packages key: nuget-${{ hashFiles('*/*.csproj') }} @@ -98,7 +98,7 @@ jobs: exit $p.ExitCode } - - uses: codecov/codecov-action@v4.0.0-beta.3 + - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c11c5dea2..c5ecdc3d6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,11 @@ 更新履歴 +==== Ver 3.15.0(2024/06/14) + * NEW: Misskeyでのノート投稿時のファイル添付に対応しました + - 追加で必要な権限があるため、前バージョンから使用している Misskey アカウントは再度追加し直す必要があります + * FIX: Favoritesタブが空のまま更新されない不具合を修正 + * FIX: 検索タブのクエリ入力欄で日本語入力をオンにできない不具合を修正 + ==== Ver 3.14.0(2024/06/11) * NEW: メインアカウント以外のホームタイムライン表示に対応 - タブ単位で切り替わるマルチアカウント機能です diff --git a/OpenTween.Tests/Api/Misskey/DriveFileCreateRequestTest.cs b/OpenTween.Tests/Api/Misskey/DriveFileCreateRequestTest.cs new file mode 100644 index 000000000..2a167d68a --- /dev/null +++ b/OpenTween.Tests/Api/Misskey/DriveFileCreateRequestTest.cs @@ -0,0 +1,74 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using OpenTween.Connection; +using Xunit; + +namespace OpenTween.Api.Misskey +{ + public class DriveFileCreateRequestTest + { + [Fact] + public async Task Send_Test() + { + using var mediaItem = TestUtils.CreateDummyMediaItem(); + + var response = TestUtils.CreateApiResponse(new MisskeyDriveFile()); + + var mock = new Mock(); + mock.Setup(x => + x.ThrowIfUnauthorizedScope("write:drive") + ); + mock.Setup(x => + x.SendAsync(It.IsAny()) + ) + .Callback(x => + { + var request = Assert.IsType(x); + Assert.Equal(new("drive/files/create", UriKind.Relative), request.RequestUri); + var expectedQuery = new Dictionary + { + ["comment"] = "tetete", + }; + Assert.Equal(expectedQuery, request.Query); + var expectedMedia = new Dictionary + { + ["file"] = mediaItem, + }; + Assert.Equal(expectedMedia, request.Media); + }) + .ReturnsAsync(response); + + var request = new DriveFileCreateRequest + { + File = mediaItem, + Comment = "tetete", + }; + await request.Send(mock.Object); + + mock.VerifyAll(); + } + } +} diff --git a/OpenTween.Tests/Api/Misskey/NoteCreateRequestTest.cs b/OpenTween.Tests/Api/Misskey/NoteCreateRequestTest.cs index d020c8ad7..f3dfac575 100644 --- a/OpenTween.Tests/Api/Misskey/NoteCreateRequestTest.cs +++ b/OpenTween.Tests/Api/Misskey/NoteCreateRequestTest.cs @@ -23,6 +23,7 @@ using System.Threading.Tasks; using Moq; using OpenTween.Connection; +using OpenTween.SocialProtocol.Misskey; using Xunit; namespace OpenTween.Api.Misskey @@ -43,7 +44,7 @@ public async Task Send_Test() var request = Assert.IsType(x); Assert.Equal(new("notes/create", UriKind.Relative), request.RequestUri); Assert.Equal( - """{"replyId":"aaaaa","text":"tetete","visibility":"public"}""", + """{"fileIds":["bbbbb"],"replyId":"aaaaa","text":"tetete","visibility":"public"}""", request.JsonString ); }) @@ -54,6 +55,36 @@ public async Task Send_Test() Text = "tetete", Visibility = "public", ReplyId = new("aaaaa"), + FileIds = new[] { new MisskeyFileId("bbbbb") }, + }; + await request.Send(mock.Object); + + mock.VerifyAll(); + } + + [Fact] + public async Task Send_RenoteTest() + { + var response = TestUtils.CreateApiResponse(new MisskeyNote()); + + var mock = new Mock(); + mock.Setup(x => + x.SendAsync(It.IsAny()) + ) + .Callback(x => + { + var request = Assert.IsType(x); + Assert.Equal(new("notes/create", UriKind.Relative), request.RequestUri); + Assert.Equal( + """{"renoteId":"aaaaa"}""", + request.JsonString + ); + }) + .ReturnsAsync(response); + + var request = new NoteCreateRequest + { + RenoteId = new("aaaaa"), }; await request.Send(mock.Object); diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index 2177a36d2..21b9c931d 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -33,6 +33,7 @@ using OpenTween.Api.DataModel; using OpenTween.Connection; using OpenTween.Models; +using OpenTween.SocialProtocol.Twitter; using Xunit; namespace OpenTween.Api @@ -275,7 +276,7 @@ public async Task StatusesUpdate_Test() await twitterApi.StatusesUpdate( "hogehoge", replyToId: new("100"), - mediaIds: new[] { 10L, 20L }, + mediaIds: new TwitterMediaId[] { new("10"), new("20") }, autoPopulateReplyMetadata: true, excludeReplyUserIds: new TwitterUserId[] { new("100"), new("200") }, attachmentUrl: "https://twitter.com/twitterapi/status/22634515958" @@ -771,7 +772,7 @@ public async Task DirectMessagesEventsNew_Test() using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.DirectMessagesEventsNew(recipientId: new("12345"), text: "hogehoge", mediaId: 67890L); + await twitterApi.DirectMessagesEventsNew(recipientId: new("12345"), text: "hogehoge", mediaId: new("67890")); mock.VerifyAll(); } @@ -1348,7 +1349,7 @@ public async Task MediaUploadAppend_Test() using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MediaUploadAppend(mediaId: 11111L, segmentIndex: 1, media: media); + await twitterApi.MediaUploadAppend(mediaId: new("11111"), segmentIndex: 1, media: media); mock.VerifyAll(); } @@ -1370,7 +1371,7 @@ public async Task MediaUploadFinalize_Test() using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MediaUploadFinalize(mediaId: 11111L) + await twitterApi.MediaUploadFinalize(mediaId: new("11111")) .IgnoreResponse(); mock.VerifyAll(); @@ -1396,7 +1397,7 @@ public async Task MediaUploadStatus_Test() using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MediaUploadStatus(mediaId: 11111L); + await twitterApi.MediaUploadStatus(mediaId: new("11111")); mock.VerifyAll(); } @@ -1413,7 +1414,7 @@ public async Task MediaMetadataCreate_Test() using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MediaMetadataCreate(mediaId: 12345L, altText: "hogehoge"); + await twitterApi.MediaMetadataCreate(mediaId: new("12345"), altText: "hogehoge"); mock.VerifyAll(); } diff --git a/OpenTween.Tests/Connection/MisskeyApiConnectionTest.cs b/OpenTween.Tests/Connection/MisskeyApiConnectionTest.cs new file mode 100644 index 000000000..d09dd4b39 --- /dev/null +++ b/OpenTween.Tests/Connection/MisskeyApiConnectionTest.cs @@ -0,0 +1,55 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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. + +using OpenTween.SocialProtocol.Misskey; +using Xunit; + +namespace OpenTween.Connection +{ + public class MisskeyApiConnectionTest + { + [Fact] + public void ThrowIfUnauthorizedScope_SuccessTest() + { + var accountState = new MisskeyAccountState + { + AuthorizedScopes = new[] { "read:account" }, + }; + using var apiConnection = new MisskeyApiConnection(new("https://example.com/api/"), "aaa", accountState); + + apiConnection.ThrowIfUnauthorizedScope("read:account"); + } + + [Fact] + public void ThrowIfUnauthorizedScope_ErrorTest() + { + var accountState = new MisskeyAccountState + { + AuthorizedScopes = new[] { "read:account" }, + }; + using var apiConnection = new MisskeyApiConnection(new("https://example.com/api/"), "aaa", accountState); + + Assert.Throws( + () => apiConnection.ThrowIfUnauthorizedScope("write:drive") + ); + } + } +} diff --git a/OpenTween.Tests/MediaSelectorTest.cs b/OpenTween.Tests/MediaSelectorTest.cs index 44d68b365..1324d6d6b 100644 --- a/OpenTween.Tests/MediaSelectorTest.cs +++ b/OpenTween.Tests/MediaSelectorTest.cs @@ -56,10 +56,10 @@ public void SelectedMediaServiceIndex_Test() mediaSelector.InitializeServices(twAccount); Assert.Equal("Twitter", mediaSelector.MediaServices[0].Key); - Assert.Equal("Imgur", mediaSelector.MediaServices[1].Key); + Assert.Equal("Imgur", mediaSelector.MediaServices[2].Key); mediaSelector.SelectedMediaServiceName = "Imgur"; - Assert.Equal(1, mediaSelector.SelectedMediaServiceIndex); + Assert.Equal(2, mediaSelector.SelectedMediaServiceIndex); mediaSelector.SelectedMediaServiceName = "Twitter"; Assert.Equal(0, mediaSelector.SelectedMediaServiceIndex); diff --git a/OpenTween.Tests/SocialProtocol/Twitter/CreateTweetFormatterTest.cs b/OpenTween.Tests/SocialProtocol/Twitter/CreateTweetFormatterTest.cs index 300073370..4a635887e 100644 --- a/OpenTween.Tests/SocialProtocol/Twitter/CreateTweetFormatterTest.cs +++ b/OpenTween.Tests/SocialProtocol/Twitter/CreateTweetFormatterTest.cs @@ -165,11 +165,11 @@ public void CreateParams_RemoveAttachmentUrl_WithMediaTest() // 引用ツイートと画像添付は併用できないため attachment_url は使用しない(現在は許容されているかも?) var postParams = new PostStatusParams(Text: "hoge https://twitter.com/twitterapi/status/22634515958") { - MediaIds = new[] { 1234L }, + MediaIds = new[] { new TwitterMediaId("1234") }, }; var expected = new CreateTweetParams(Text: "hoge https://twitter.com/twitterapi/status/22634515958") { - MediaIds = new[] { 1234L }, + MediaIds = new[] { new TwitterMediaId("1234") }, }; Assert.Equal(expected, formatter.CreateParams(postParams)); } diff --git a/OpenTween/Api/Misskey/DriveFileCreateRequest.cs b/OpenTween/Api/Misskey/DriveFileCreateRequest.cs new file mode 100644 index 000000000..58be5aec1 --- /dev/null +++ b/OpenTween/Api/Misskey/DriveFileCreateRequest.cs @@ -0,0 +1,70 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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.Threading.Tasks; +using OpenTween.Connection; + +namespace OpenTween.Api.Misskey +{ + public class DriveFileCreateRequest + { + public required IMediaItem File { get; set; } + + public string? Comment { get; set; } + + public async Task Send(IApiConnection apiConnection) + { + apiConnection.ThrowIfUnauthorizedScope("write:drive"); + + var request = new PostMultipartRequest + { + RequestUri = new("drive/files/create", UriKind.Relative), + Query = this.CreateQuery(), + Media = new Dictionary + { + ["file"] = this.File, + }, + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var responseBody = await response.ReadAsJson() + .ConfigureAwait(false); + + return responseBody; + } + + private IDictionary CreateQuery() + { + var query = new Dictionary(); + + if (!MyCommon.IsNullOrEmpty(this.Comment)) + query["comment"] = this.Comment; + + return query; + } + } +} diff --git a/OpenTween/Api/Misskey/NoteCreateRequest.cs b/OpenTween/Api/Misskey/NoteCreateRequest.cs index 858e6602f..93d3435f2 100644 --- a/OpenTween/Api/Misskey/NoteCreateRequest.cs +++ b/OpenTween/Api/Misskey/NoteCreateRequest.cs @@ -22,6 +22,7 @@ #nullable enable using System; +using System.Linq; using System.Runtime.Serialization; using System.Threading.Tasks; using OpenTween.Connection; @@ -39,6 +40,8 @@ public class NoteCreateRequest public MisskeyNoteId? RenoteId { get; set; } + public MisskeyFileId[] FileIds { get; set; } = Array.Empty(); + public async Task Send(IApiConnection apiConnection) { var request = new PostJsonRequest @@ -65,7 +68,9 @@ private record RequestBody( [property: DataMember(Name = "replyId", EmitDefaultValue = false)] string? ReplyId, [property: DataMember(Name = "renoteId", EmitDefaultValue = false)] - string? RenoteId + string? RenoteId, + [property: DataMember(Name = "fileIds", EmitDefaultValue = false)] + string[]? FileIds ); [DataContract] @@ -77,11 +82,16 @@ private class ResponseBody private string CreateRequestJson() { + var fileIds = this.FileIds.Length > 0 + ? this.FileIds.Select(x => x.Id).ToArray() + : null; + var body = new RequestBody( Text: this.Text, Visibility: this.Visibility, ReplyId: this.ReplyId?.Id, - RenoteId: this.RenoteId?.Id + RenoteId: this.RenoteId?.Id, + FileIds: fileIds ); return JsonUtils.SerializeJsonByDataContract(body); diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 3277b8d7e..a53d070f1 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -32,6 +32,7 @@ using OpenTween.Api.DataModel; using OpenTween.Connection; using OpenTween.Models; +using OpenTween.SocialProtocol.Twitter; namespace OpenTween.Api { @@ -191,7 +192,7 @@ public async Task StatusesLookup(IReadOnlyList statusId public async Task> StatusesUpdate( string status, TwitterStatusId? replyToId, - IReadOnlyList? mediaIds, + IReadOnlyList? mediaIds, bool? autoPopulateReplyMetadata = null, IReadOnlyList? excludeReplyUserIds = null, string? attachmentUrl = null) @@ -207,7 +208,7 @@ public async Task> StatusesUpdate( if (replyToId != null) param["in_reply_to_status_id"] = replyToId.Id; if (mediaIds != null && mediaIds.Count > 0) - param.Add("media_ids", string.Join(",", mediaIds)); + param.Add("media_ids", string.Join(",", mediaIds.Select(x => x.Id))); if (autoPopulateReplyMetadata != null) param["auto_populate_reply_metadata"] = autoPopulateReplyMetadata.Value ? "true" : "false"; if (excludeReplyUserIds != null && excludeReplyUserIds.Count > 0) @@ -613,7 +614,7 @@ public async Task DirectMessagesEventsList(int? count = .ConfigureAwait(false); } - public async Task> DirectMessagesEventsNew(TwitterUserId recipientId, string text, long? mediaId = null) + public async Task> DirectMessagesEventsNew(TwitterUserId recipientId, string text, TwitterMediaId? mediaId = null) { var attachment = ""; if (mediaId != null) @@ -622,7 +623,7 @@ public async Task> DirectMessagesEventsNew(T "attachment": { "type": "media", "media": { - "id": "{{JsonUtils.EscapeJsonString(mediaId.ToString())}}" + "id": "{{JsonUtils.EscapeJsonString(mediaId.Id)}}" } } """; @@ -1106,7 +1107,7 @@ public async Task> MediaUploadInit(long totalBy return response.ReadAsLazyJson(); } - public async Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem media) + public async Task MediaUploadAppend(TwitterMediaId mediaId, int segmentIndex, IMediaItem media) { var request = new PostMultipartRequest { @@ -1114,7 +1115,7 @@ public async Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem m Query = new Dictionary { ["command"] = "APPEND", - ["media_id"] = mediaId.ToString(), + ["media_id"] = mediaId.Id, ["segment_index"] = segmentIndex.ToString(), }, Media = new Dictionary @@ -1128,7 +1129,7 @@ await this.Connection.SendAsync(request) .ConfigureAwait(false); } - public async Task> MediaUploadFinalize(long mediaId) + public async Task> MediaUploadFinalize(TwitterMediaId mediaId) { var request = new PostRequest { @@ -1136,7 +1137,7 @@ public async Task> MediaUploadFinalize(long m Query = new Dictionary { ["command"] = "FINALIZE", - ["media_id"] = mediaId.ToString(), + ["media_id"] = mediaId.Id, }, }; @@ -1146,7 +1147,7 @@ public async Task> MediaUploadFinalize(long m return response.ReadAsLazyJson(); } - public async Task MediaUploadStatus(long mediaId) + public async Task MediaUploadStatus(TwitterMediaId mediaId) { var request = new GetRequest { @@ -1154,7 +1155,7 @@ public async Task MediaUploadStatus(long mediaId) Query = new Dictionary { ["command"] = "STATUS", - ["media_id"] = mediaId.ToString(), + ["media_id"] = mediaId.Id, }, }; @@ -1165,13 +1166,14 @@ public async Task MediaUploadStatus(long mediaId) .ConfigureAwait(false); } - public async Task MediaMetadataCreate(long mediaId, string altText) + public async Task MediaMetadataCreate(TwitterMediaId mediaId, string altText) { + var escapedMediaId = JsonUtils.EscapeJsonString(mediaId.Id); var escapedAltText = JsonUtils.EscapeJsonString(altText); var request = new PostJsonRequest { RequestUri = new("https://upload.twitter.com/1.1/media/metadata/create.json"), - JsonString = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}""", + JsonString = $$$"""{"media_id": "{{{escapedMediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}""", }; await this.Connection.SendAsync(request) diff --git a/OpenTween/Connection/AdditionalScopeRequiredException.cs b/OpenTween/Connection/AdditionalScopeRequiredException.cs new file mode 100644 index 000000000..bdd1b154d --- /dev/null +++ b/OpenTween/Connection/AdditionalScopeRequiredException.cs @@ -0,0 +1,33 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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 + +namespace OpenTween.Connection +{ + public class AdditionalScopeRequiredException : WebApiException + { + public AdditionalScopeRequiredException() + : base(Properties.Resources.AdditionalScopeRequired_Message) + { + } + } +} diff --git a/OpenTween/Connection/IApiConnection.cs b/OpenTween/Connection/IApiConnection.cs index 16e816668..ea58a7e61 100644 --- a/OpenTween/Connection/IApiConnection.cs +++ b/OpenTween/Connection/IApiConnection.cs @@ -28,6 +28,8 @@ namespace OpenTween.Connection { public interface IApiConnection : IDisposable { + void ThrowIfUnauthorizedScope(string scope); + Task SendAsync(IHttpRequest request); } } diff --git a/OpenTween/Connection/MisskeyApiConnection.cs b/OpenTween/Connection/MisskeyApiConnection.cs index 366e75194..400085be2 100644 --- a/OpenTween/Connection/MisskeyApiConnection.cs +++ b/OpenTween/Connection/MisskeyApiConnection.cs @@ -23,12 +23,14 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Net.Cache; using System.Net.Http; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using OpenTween.Api.Misskey; +using OpenTween.SocialProtocol.Misskey; namespace OpenTween.Connection { @@ -40,11 +42,13 @@ public sealed class MisskeyApiConnection : IApiConnection, IDisposable private readonly Uri apiBaseUri; private readonly string accessToken; + private readonly MisskeyAccountState accountState; - public MisskeyApiConnection(Uri apiBaseUri, string accessToken) + public MisskeyApiConnection(Uri apiBaseUri, string accessToken, MisskeyAccountState accountState) { this.apiBaseUri = apiBaseUri; this.accessToken = accessToken; + this.accountState = accountState; this.InitializeHttpClients(); Networking.WebProxyChanged += this.Networking_WebProxyChanged; @@ -59,6 +63,12 @@ private void InitializeHttpClients() this.Http.Timeout = Timeout.InfiniteTimeSpan; } + public void ThrowIfUnauthorizedScope(string scope) + { + if (!this.accountState.AuthorizedScopes.Contains(scope)) + throw new AdditionalScopeRequiredException(); + } + public async Task SendAsync(IHttpRequest request) { using var requestMessage = request.CreateMessage(this.apiBaseUri); diff --git a/OpenTween/Connection/TwitterApiConnection.cs b/OpenTween/Connection/TwitterApiConnection.cs index d7818acc3..839b48777 100644 --- a/OpenTween/Connection/TwitterApiConnection.cs +++ b/OpenTween/Connection/TwitterApiConnection.cs @@ -82,6 +82,10 @@ private void InitializeHttpClients() this.Http.Timeout = Timeout.InfiniteTimeSpan; } + public void ThrowIfUnauthorizedScope(string scope) + { + } + public async Task SendAsync(IHttpRequest request) { var endpointName = request.EndpointName; diff --git a/OpenTween/Controls/PublicSearchHeaderPanel.resx b/OpenTween/Controls/PublicSearchHeaderPanel.resx index e06e90635..b23adc575 100644 --- a/OpenTween/Controls/PublicSearchHeaderPanel.resx +++ b/OpenTween/Controls/PublicSearchHeaderPanel.resx @@ -66,6 +66,7 @@ 50, 20 2 Left, Right + NoControl 89, 1 0, 0, 0, 0 187, 20 diff --git a/OpenTween/MediaSelector.cs b/OpenTween/MediaSelector.cs index 7fceb534e..3439ec2a4 100644 --- a/OpenTween/MediaSelector.cs +++ b/OpenTween/MediaSelector.cs @@ -33,6 +33,7 @@ using OpenTween.Api.DataModel; using OpenTween.MediaUploadServices; using OpenTween.SocialProtocol; +using OpenTween.SocialProtocol.Misskey; using OpenTween.SocialProtocol.Twitter; namespace OpenTween @@ -121,25 +122,26 @@ public MemoryImage? SelectedMediaItemImage public void InitializeServices(ISocialAccount account) { + var mediaServices = new List>(); + if (account is TwitterAccount twAccount) { var twLegacy = twAccount.Legacy; var twConfiguration = twAccount.AccountState.Configuration; - this.MediaServices = new KeyValuePair[] + mediaServices.AddRange(new KeyValuePair[] { new("Twitter", new TwitterPhoto(twLegacy, twConfiguration)), - new("Imgur", new Imgur(twConfiguration)), new("Mobypicture", new Mobypicture(twLegacy, twConfiguration)), - }; - } - else - { - this.MediaServices = new KeyValuePair[] - { - new("Imgur", new Imgur(TwitterConfiguration.DefaultConfiguration())), - }; + }); } + + if (account is MisskeyAccount misskeyAccount) + mediaServices.Add(new("Misskey", new MisskeyDrive(misskeyAccount))); + + mediaServices.Add(new("Imgur", new Imgur(TwitterConfiguration.DefaultConfiguration()))); + + this.MediaServices = mediaServices.ToArray(); } public void SelectMediaService(string serviceName, int? index = null) diff --git a/OpenTween/MediaUploadServices/MisskeyDrive.cs b/OpenTween/MediaUploadServices/MisskeyDrive.cs new file mode 100644 index 000000000..e5ad0e6fa --- /dev/null +++ b/OpenTween/MediaUploadServices/MisskeyDrive.cs @@ -0,0 +1,118 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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.DataModel; +using OpenTween.Api.Misskey; +using OpenTween.SocialProtocol.Misskey; + +namespace OpenTween.MediaUploadServices +{ + public class MisskeyDrive : IMediaUploadService + { + private readonly string[] pictureExt = { ".jpg", ".jpeg", ".gif", ".png" }; + + private readonly MisskeyAccount account; + + public MisskeyDrive(MisskeyAccount account) + => this.account = account; + + public int MaxMediaCount => 4; + + public string SupportedFormatsStrForDialog => "Image Files(*.gif;*.jpg;*.jpeg;*.png)|*.gif;*.jpg;*.jpeg;*.png"; + + public bool CanUseAltText => true; + + public bool IsNativeUploadService => true; + + public bool CheckFileExtension(string fileExtension) + => this.pictureExt.Contains(fileExtension, StringComparer.InvariantCultureIgnoreCase); + + public bool CheckFileSize(string fileExtension, long fileSize) + { + var maxFileSize = this.GetMaxFileSize(fileExtension); + return maxFileSize == null || fileSize <= maxFileSize.Value; + } + + public long? GetMaxFileSize(string fileExtension) + => 3145728L; // 3MiB + + public async Task UploadAsync(IMediaItem[] mediaItems, PostStatusParams postParams) + { + if (mediaItems == null) + throw new ArgumentNullException(nameof(mediaItems)); + + if (mediaItems.Length == 0) + throw new ArgumentException("Err:Media not specified."); + + foreach (var item in mediaItems) + { + if (item == null) + throw new ArgumentException("Err:Media not specified."); + + if (!item.Exists) + throw new ArgumentException("Err:Media not found."); + } + + var misskeyFileIds = await this.UploadDriveFiles(mediaItems) + .ConfigureAwait(false); + + return postParams with { MediaIds = misskeyFileIds }; + } + + // pic.twitter.com の URL は文字数にカウントされない + public int GetReservedTextLength(int mediaCount) + => 0; + + public void UpdateTwitterConfiguration(TwitterConfiguration config) + { + } + + private async Task UploadDriveFiles(IMediaItem[] mediaItems) + { + var uploadTasks = from m in mediaItems + select this.UploadMediaItem(m); + + var misskeyFileIds = await Task.WhenAll(uploadTasks) + .ConfigureAwait(false); + + return misskeyFileIds; + } + + private async Task UploadMediaItem(IMediaItem mediaItem) + { + var request = new DriveFileCreateRequest + { + File = mediaItem, + Comment = mediaItem.AltText, + }; + + var misskeyFile = await request.Send(this.account.Connection) + .ConfigureAwait(false); + + return new(misskeyFile.Id); + } + } +} diff --git a/OpenTween/MediaUploadServices/TwitterPhoto.cs b/OpenTween/MediaUploadServices/TwitterPhoto.cs index 717f5b4df..375451963 100644 --- a/OpenTween/MediaUploadServices/TwitterPhoto.cs +++ b/OpenTween/MediaUploadServices/TwitterPhoto.cs @@ -36,6 +36,7 @@ using System.Threading.Tasks; using OpenTween.Api.DataModel; using OpenTween.Setting; +using OpenTween.SocialProtocol.Twitter; namespace OpenTween.MediaUploadServices { @@ -89,7 +90,7 @@ public async Task UploadAsync(IMediaItem[] mediaItems, PostSta throw new ArgumentException("Err:Media not found."); } - long[] mediaIds; + TwitterMediaId[] mediaIds; if (Twitter.DMSendTextRegex.IsMatch(postParams.Text)) mediaIds = new[] { await this.UploadMediaForDM(mediaItems).ConfigureAwait(false) }; @@ -106,7 +107,7 @@ public int GetReservedTextLength(int mediaCount) public void UpdateTwitterConfiguration(TwitterConfiguration config) => this.twitterConfig = config; - private async Task UploadMediaForTweet(IMediaItem[] mediaItems) + private async Task UploadMediaForTweet(IMediaItem[] mediaItems) { var uploadTasks = from m in mediaItems select this.UploadMediaItem(m, mediaCategory: null); @@ -117,7 +118,7 @@ private async Task UploadMediaForTweet(IMediaItem[] mediaItems) return mediaIds; } - private async Task UploadMediaForDM(IMediaItem[] mediaItems) + private async Task UploadMediaForDM(IMediaItem[] mediaItems) { if (mediaItems.Length > 1) throw new InvalidOperationException("Err:Can't attach multiple media to DM."); @@ -135,9 +136,9 @@ private async Task UploadMediaForDM(IMediaItem[] mediaItems) return mediaId; } - private async Task UploadMediaItem(IMediaItem mediaItem, string? mediaCategory) + private async Task UploadMediaItem(IMediaItem mediaItem, string? mediaCategory) { - async Task UploadInternal(IMediaItem media, string? category) + async Task UploadInternal(IMediaItem media, string? category) { var mediaId = await this.tw.UploadMedia(media, category) .ConfigureAwait(false); diff --git a/OpenTween/Models/INativeUploadMediaId.cs b/OpenTween/Models/INativeUploadMediaId.cs new file mode 100644 index 000000000..6a427390c --- /dev/null +++ b/OpenTween/Models/INativeUploadMediaId.cs @@ -0,0 +1,30 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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 + +namespace OpenTween.Models +{ + public interface INativeUploadMediaId + { + public string Id { get; } + } +} diff --git a/OpenTween/PostStatusParams.cs b/OpenTween/PostStatusParams.cs index 87419ea1d..1e60ad7be 100644 --- a/OpenTween/PostStatusParams.cs +++ b/OpenTween/PostStatusParams.cs @@ -23,10 +23,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using OpenTween.Connection; using OpenTween.Models; namespace OpenTween @@ -34,9 +30,9 @@ namespace OpenTween public record PostStatusParams( string Text, PostClass? InReplyTo = null, - IReadOnlyList? MediaIds = null + IReadOnlyList? MediaIds = null ) { - public IReadOnlyList MediaIds { get; init; } = MediaIds ?? Array.Empty(); + public IReadOnlyList MediaIds { get; init; } = MediaIds ?? Array.Empty(); } } diff --git a/OpenTween/Properties/AssemblyInfo.cs b/OpenTween/Properties/AssemblyInfo.cs index 02cdca780..ef93077e6 100644 --- a/OpenTween/Properties/AssemblyInfo.cs +++ b/OpenTween/Properties/AssemblyInfo.cs @@ -22,7 +22,7 @@ // 次の GUID は、このプロジェクトが COM に公開される場合の、typelib の ID です [assembly: Guid("2d0ae0ba-adac-49a2-9b10-26fd69e695bf")] -[assembly: AssemblyVersion("3.14.0.0")] +[assembly: AssemblyVersion("3.15.0.0")] [assembly: InternalsVisibleTo("OpenTween.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Moq diff --git a/OpenTween/Properties/Resources.Designer.cs b/OpenTween/Properties/Resources.Designer.cs index f37073a63..1ad0d302d 100644 --- a/OpenTween/Properties/Resources.Designer.cs +++ b/OpenTween/Properties/Resources.Designer.cs @@ -96,6 +96,15 @@ internal static string AccountTypeErrorText { } } + /// + /// この機能を使用するためにはアカウントの再設定が必要です に類似しているローカライズされた文字列を検索します。 + /// + internal static string AdditionalScopeRequired_Message { + get { + return ResourceManager.GetString("AdditionalScopeRequired_Message", resourceCulture); + } + } + /// /// 発言一覧 に類似しているローカライズされた文字列を検索します。 /// @@ -616,6 +625,12 @@ internal static string ChangeIconToolStripMenuItem_Confirm { /// /// 更新履歴 /// + ///==== Ver 3.15.0(2024/06/14) + /// * NEW: Misskeyでのノート投稿時のファイル添付に対応しました + /// - 追加で必要な権限があるため、前バージョンから使用している Misskey アカウントは再度追加し直す必要があります + /// * FIX: Favoritesタブが空のまま更新されない不具合を修正 + /// * FIX: 検索タブのクエリ入力欄で日本語入力をオンにできない不具合を修正 + /// ///==== Ver 3.14.0(2024/06/11) /// * NEW: メインアカウント以外のホームタイムライン表示に対応 /// - タブ単位で切り替わるマルチアカウント機能です @@ -623,13 +638,7 @@ internal static string ChangeIconToolStripMenuItem_Confirm { /// - 現時点ではメインアカウント以外のタブ設定は次回起動時に保持されません /// * NEW: Misskeyアカウントのホームタイムライン表示・投稿に対応しました /// - 現時点では Misskey アカウントをメインに設定することはできません - /// - MFMの表示には対応していません - /// * NEW: Twemoji 15.1.0 に対応しました - /// - Unicode 15.1 で追加された絵文字が表示されるようになります - /// * NEW: WebP画像の表示に対応しました - /// - プロフィール画像やサムネイル画像にWebPが使われている場合も表示が可能になります - /// - 「WebP画像拡張機能」がインストールされている環境でのみ動作します - /// * CHG: 設定画面でのアカウント一覧の表 [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 + /// - MFMの表示には対応していません [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 /// internal static string ChangeLog { get { diff --git a/OpenTween/Properties/Resources.en.resx b/OpenTween/Properties/Resources.en.resx index 7a416dd78..7eb57dc21 100644 --- a/OpenTween/Properties/Resources.en.resx +++ b/OpenTween/Properties/Resources.en.resx @@ -8,6 +8,7 @@ (Disabled) (Primary) This feature is not available for current account. + You will need to re-authorize the account to use this feature. PostsList (New Tab) Name diff --git a/OpenTween/Properties/Resources.resx b/OpenTween/Properties/Resources.resx index 31b6aeccf..4f938fb5c 100644 --- a/OpenTween/Properties/Resources.resx +++ b/OpenTween/Properties/Resources.resx @@ -9,6 +9,7 @@ (無効) (メイン) この機能は現在のアカウントでは使用できません + この機能を使用するためにはアカウントの再設定が必要です 発言一覧 (新規タブ) 名前 diff --git a/OpenTween/SocialProtocol/AccountCollection.cs b/OpenTween/SocialProtocol/AccountCollection.cs index 8ff600475..7bc88235a 100644 --- a/OpenTween/SocialProtocol/AccountCollection.cs +++ b/OpenTween/SocialProtocol/AccountCollection.cs @@ -108,12 +108,12 @@ public ISocialAccount GetAccountForTab(TabModel tab) return this.Primary; } - public ISocialAccount? GetAccountForPostId(PostId postId, AccountKey? preferedAccountKey) + public ISocialAccount? GetAccountForPostId(PostId postId, AccountKey? preferredAccountKey) { - if (preferedAccountKey != null && this.accounts.TryGetValue(preferedAccountKey.Value, out var preferedAccount)) + if (preferredAccountKey != null && this.accounts.TryGetValue(preferredAccountKey.Value, out var preferredAccount)) { - if (preferedAccount.CanUsePostId(postId)) - return preferedAccount; + if (preferredAccount.CanUsePostId(postId)) + return preferredAccount; } var primaryAccount = this.Primary; diff --git a/OpenTween/SocialProtocol/InvalidAccount.cs b/OpenTween/SocialProtocol/InvalidAccount.cs index 802b04667..24d31a23d 100644 --- a/OpenTween/SocialProtocol/InvalidAccount.cs +++ b/OpenTween/SocialProtocol/InvalidAccount.cs @@ -65,6 +65,10 @@ public void Dispose() private class InvalidAccountConnection : IApiConnection { + public void ThrowIfUnauthorizedScope(string scope) + { + } + public Task SendAsync(IHttpRequest request) => throw new WebApiException("Invalid account"); diff --git a/OpenTween/SocialProtocol/Misskey/MisskeyAccount.cs b/OpenTween/SocialProtocol/Misskey/MisskeyAccount.cs index ad7ff336f..3aed2b624 100644 --- a/OpenTween/SocialProtocol/Misskey/MisskeyAccount.cs +++ b/OpenTween/SocialProtocol/Misskey/MisskeyAccount.cs @@ -74,12 +74,13 @@ public void Initialize(UserAccount accountSettings, SettingCommon settingCommon) var serverUri = new Uri($"https://{accountSettings.ServerHostname}/"); this.AccountState = new(serverUri, new(accountSettings.UserId), accountSettings.Username) { + AuthorizedScopes = accountSettings.Scopes, HasUnrecoverableError = false, }; var apiBaseUri = new Uri(serverUri, "/api/"); - var newConnection = new MisskeyApiConnection(apiBaseUri, accountSettings.TokenSecret); + var newConnection = new MisskeyApiConnection(apiBaseUri, accountSettings.TokenSecret, this.AccountState); (this.connection, var oldConnection) = (newConnection, this.connection); oldConnection?.Dispose(); } diff --git a/OpenTween/SocialProtocol/Misskey/MisskeyAccountState.cs b/OpenTween/SocialProtocol/Misskey/MisskeyAccountState.cs index 2ea7857f0..972f54c2d 100644 --- a/OpenTween/SocialProtocol/Misskey/MisskeyAccountState.cs +++ b/OpenTween/SocialProtocol/Misskey/MisskeyAccountState.cs @@ -40,6 +40,8 @@ PersonId ISocialAccountState.UserId public string UserName { get; private set; } + public string[] AuthorizedScopes { get; set; } = Array.Empty(); + public int? FollowersCount { get; private set; } public int? FriendsCount { get; private set; } diff --git a/OpenTween/SocialProtocol/Misskey/MisskeyClient.cs b/OpenTween/SocialProtocol/Misskey/MisskeyClient.cs index 4b20c30e0..a0c160cd3 100644 --- a/OpenTween/SocialProtocol/Misskey/MisskeyClient.cs +++ b/OpenTween/SocialProtocol/Misskey/MisskeyClient.cs @@ -109,6 +109,7 @@ public Task GetRelatedPosts(PostClass targetPost, bool firstLoad) ReplyId = postParams.InReplyTo is { } replyTo ? this.AssertMisskeyNoteId(replyTo.StatusId) : null, + FileIds = postParams.MediaIds.Cast().ToArray(), }; var note = await request.Send(this.account.Connection) diff --git a/OpenTween/SocialProtocol/Misskey/MisskeyFileId.cs b/OpenTween/SocialProtocol/Misskey/MisskeyFileId.cs new file mode 100644 index 000000000..fbb988bd0 --- /dev/null +++ b/OpenTween/SocialProtocol/Misskey/MisskeyFileId.cs @@ -0,0 +1,31 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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 OpenTween.Models; + +namespace OpenTween.SocialProtocol.Misskey +{ + public record MisskeyFileId( + string Id + ) : INativeUploadMediaId; +} diff --git a/OpenTween/SocialProtocol/Misskey/MisskeySetup.cs b/OpenTween/SocialProtocol/Misskey/MisskeySetup.cs index 1d0659fa2..3575a8f79 100644 --- a/OpenTween/SocialProtocol/Misskey/MisskeySetup.cs +++ b/OpenTween/SocialProtocol/Misskey/MisskeySetup.cs @@ -35,6 +35,7 @@ public class MisskeySetup : NotifyPropertyChangedBase public static readonly string[] AuthorizeScopes = new[] { "read:account", + "write:drive", "write:notes", "write:reactions", }; @@ -107,7 +108,7 @@ public async Task DoAuthorize() throw new InvalidOperationException($"{nameof(this.serverBaseUri)} is null"); var apiBaseUri = new Uri(this.serverBaseUri, "/api/"); - var apiConnection = new MisskeyApiConnection(apiBaseUri, accessToken: ""); + var apiConnection = new MisskeyApiConnection(apiBaseUri, accessToken: "", new()); var request = new MiauthCheckRequest { SessionNonce = this.sessionNonce.ToString(), diff --git a/OpenTween/SocialProtocol/Twitter/CreateTweetFormatter.cs b/OpenTween/SocialProtocol/Twitter/CreateTweetFormatter.cs index 8308b1821..0b5a45785 100644 --- a/OpenTween/SocialProtocol/Twitter/CreateTweetFormatter.cs +++ b/OpenTween/SocialProtocol/Twitter/CreateTweetFormatter.cs @@ -52,7 +52,7 @@ public CreateTweetParams CreateParams(PostStatusParams formState) var createParams = new CreateTweetParams( formState.Text, formState.InReplyTo, - formState.MediaIds + formState.MediaIds.Cast().ToArray() ); // DM の場合はこれ以降の処理を行わない diff --git a/OpenTween/SocialProtocol/Twitter/CreateTweetParams.cs b/OpenTween/SocialProtocol/Twitter/CreateTweetParams.cs index 7a9cde7df..00b47f0fe 100644 --- a/OpenTween/SocialProtocol/Twitter/CreateTweetParams.cs +++ b/OpenTween/SocialProtocol/Twitter/CreateTweetParams.cs @@ -31,13 +31,13 @@ namespace OpenTween.SocialProtocol.Twitter public record CreateTweetParams( string Text, PostClass? InReplyTo = null, - IReadOnlyList? MediaIds = null, + IReadOnlyList? MediaIds = null, bool AutoPopulateReplyMetadata = false, IReadOnlyList? ExcludeReplyUserIds = null, string? AttachmentUrl = null ) { - public IReadOnlyList MediaIds { get; init; } = MediaIds ?? Array.Empty(); + public IReadOnlyList MediaIds { get; init; } = MediaIds ?? Array.Empty(); public IReadOnlyList ExcludeReplyUserIds { get; init; } = ExcludeReplyUserIds ?? Array.Empty(); diff --git a/OpenTween/SocialProtocol/Twitter/TwitterGraphqlClient.cs b/OpenTween/SocialProtocol/Twitter/TwitterGraphqlClient.cs index 14c496910..84acb0f59 100644 --- a/OpenTween/SocialProtocol/Twitter/TwitterGraphqlClient.cs +++ b/OpenTween/SocialProtocol/Twitter/TwitterGraphqlClient.cs @@ -150,7 +150,7 @@ public async Task GetFavoritesTimeline(int count, IQueryCursor var cursorTop = response.CursorTop; var cursorBottom = response.CursorBottom; - var posts = this.account.Legacy.CreatePostsFromJson(statuses, firstLoad); + var posts = this.account.Legacy.CreatePostsFromJson(statuses, firstLoad, favTweet: true); var filter = new TimelineResponseFilter(this.account.AccountState); posts = filter.Run(posts); diff --git a/OpenTween/SocialProtocol/Twitter/TwitterMediaId.cs b/OpenTween/SocialProtocol/Twitter/TwitterMediaId.cs new file mode 100644 index 000000000..9bbc7b636 --- /dev/null +++ b/OpenTween/SocialProtocol/Twitter/TwitterMediaId.cs @@ -0,0 +1,31 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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 OpenTween.Models; + +namespace OpenTween.SocialProtocol.Twitter +{ + public record TwitterMediaId( + string Id + ) : INativeUploadMediaId; +} diff --git a/OpenTween/SocialProtocol/Twitter/TwitterV1Client.cs b/OpenTween/SocialProtocol/Twitter/TwitterV1Client.cs index 0cd7eadd5..bc3c78741 100644 --- a/OpenTween/SocialProtocol/Twitter/TwitterV1Client.cs +++ b/OpenTween/SocialProtocol/Twitter/TwitterV1Client.cs @@ -118,7 +118,7 @@ public async Task GetFavoritesTimeline(int count, IQueryCursor .ConfigureAwait(false); var (cursorTop, cursorBottom) = GetCursorFromResponse(statuses); - var posts = this.account.Legacy.CreatePostsFromJson(statuses, firstLoad); + var posts = this.account.Legacy.CreatePostsFromJson(statuses, firstLoad, favTweet: true); var filter = new TimelineResponseFilter(this.account.AccountState); posts = filter.Run(posts); diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index e4a93e7bf..559dc160b 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -3336,13 +3336,13 @@ private PostStatusParams CreatePostStatusParams(bool setFakeMediaIds = false) { var statusText = this.StatusText.Text; var replyToPost = this.inReplyTo is (var inReplyToStatusId, _) ? this.statuses[inReplyToStatusId] : null; - var mediaIds = Array.Empty(); + var mediaIds = Array.Empty(); if (setFakeMediaIds) { // 文字数計算のために仮の mediaId を設定する var useNativeUpload = this.ImageSelector.Visible && this.ImageSelector.Model.SelectedMediaService is { IsNativeUploadService: true }; if (useNativeUpload) - mediaIds = new[] { -1L }; + mediaIds = new[] { new TwitterMediaId("") }; } var statusParams = new PostStatusParams(statusText, replyToPost, mediaIds); @@ -9004,8 +9004,8 @@ public async Task OpenRelatedTab(PostId statusId) public ISocialAccount? GetAccountForPostId(PostId postId) { - var preferedAccountKey = this.CurrentTab.SourceAccountKey; - return this.accounts.GetAccountForPostId(postId, preferedAccountKey); + var preferredAccountKey = this.CurrentTab.SourceAccountKey; + return this.accounts.GetAccountForPostId(postId, preferredAccountKey); } /// diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index adb834bc5..b014d068b 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -212,7 +212,7 @@ public void Initialize(TwitterApiConnection apiConnection, TwitterAccountState a if (Twitter.DMSendTextRegex.IsMatch(param.Text)) { - var mediaId = param.MediaIds != null && param.MediaIds.Any() ? param.MediaIds[0] : (long?)null; + var mediaId = param.MediaIds?.FirstOrDefault(); await this.SendDirectMessage(param.Text, mediaId) .ConfigureAwait(false); @@ -264,7 +264,7 @@ await this.SendDirectMessage(param.Text, mediaId) return post; } - public async Task UploadMedia(IMediaItem item, string? mediaCategory = null) + public async Task UploadMedia(IMediaItem item, string? mediaCategory = null) { this.CheckAccountState(); @@ -283,7 +283,7 @@ public async Task UploadMedia(IMediaItem item, string? mediaCategory = nul var initMedia = await initResponse.LoadJsonAsync() .ConfigureAwait(false); - var mediaId = initMedia.MediaId; + var mediaId = new TwitterMediaId(initMedia.MediaIdStr); await this.Api.MediaUploadAppend(mediaId, 0, item) .ConfigureAwait(false); @@ -318,10 +318,10 @@ await Task.Delay(TimeSpan.FromSeconds(processingInfo.CheckAfterSecs ?? 5)) } succeeded: - return media.MediaId; + return mediaId; } - public async Task SendDirectMessage(string postStr, long? mediaId = null) + public async Task SendDirectMessage(string postStr, TwitterMediaId? mediaId = null) { this.CheckAccountState(); @@ -537,8 +537,11 @@ internal PostClass CreatePostsFromStatusData(TwitterStatus status, bool firstLoa } internal PostClass[] CreatePostsFromJson(TwitterStatus[] statuses, bool firstLoad) + => this.CreatePostsFromJson(statuses, firstLoad, favTweet: false); + + internal PostClass[] CreatePostsFromJson(TwitterStatus[] statuses, bool firstLoad, bool favTweet) { - var posts = statuses.Select(x => this.CreatePostsFromStatusData(x, firstLoad)).ToArray(); + var posts = statuses.Select(x => this.CreatePostsFromStatusData(x, firstLoad, favTweet)).ToArray(); TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); diff --git a/appveyor.yml b/appveyor.yml index 8805c3a39..c4aac59fd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 3.13.0.{build} +version: 3.14.0.{build} os: Visual Studio 2022